feat: Add MCP Server Settings to projects, rename Folder to Project (#7741)

Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
Co-authored-by: deon-sanchez <deon.sanchez@datastax.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
Co-authored-by: phact <estevezsebastian@gmail.com>
This commit is contained in:
Edwin Jose 2025-04-29 13:14:55 -04:00 • committed by GitHub
commit c80cb3f35e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 4434 additions and 804 deletions

View file

@ -19,46 +19,50 @@ test(
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByPlaceholder("Search flows").first().isVisible();
await page.getByText("Flows").first().isVisible();
await page.getByText("Components").first().isVisible();
if (await page.getByText("Components").first().isVisible()) {
await page.getByText("Components").first().isVisible();
} else {
await page.getByText("MCP Server").first().isVisible();
}
await page.getByText("All").first().isVisible();
await page.getByText("Select All").first().isVisible();
await page.getByTestId("add-folder-button").click();
await page.getByTestId("add-project-button").click();
await page
.locator("[data-testid='folder-sidebar']")
.getByText("New Folder")
.locator("[data-testid='project-sidebar']")
.getByText("New Project")
.last()
.isVisible();
await page
.locator("[data-testid='folder-sidebar']")
.getByText("New Folder")
.locator("[data-testid='project-sidebar']")
.getByText("New Project")
.last()
.dblclick();
const element = await page.getByTestId("input-folder");
await element.fill("new folder test name");
const element = await page.getByTestId("input-project");
await element.fill("new project test name");
await page.getByText("My Projects").last().click({
await page.getByText("Starter Project").last().click({
force: true,
});
await page.getByText("new folder test name").last().waitFor({
await page.getByText("new project test name").last().waitFor({
state: "visible",
timeout: 30000,
});
await page
.getByText("new folder test name")
.getByText("new project test name")
.last()
.hover()
.then(async () => {
await page.getByTestId("more-options-button").last().click();
});
await page.getByTestId("btn-delete-folder").click();
await page.getByTestId("btn-delete-project").click();
await page.getByText("Delete").last().click();
await expect(page.getByText("Folder deleted successfully")).toBeVisible({
await expect(page.getByText("Project deleted successfully")).toBeVisible({
timeout: 3000,
});
},
@ -137,26 +141,28 @@ test("change flow folder", async ({ page }) => {
await page.getByPlaceholder("Search flows").isVisible();
await page.getByText("Flows").first().isVisible();
await page.getByText("Components").first().isVisible();
await page.getByText("All").first().isVisible();
await page.getByText("Select All").first().isVisible();
if (await page.getByText("Components").first().isVisible()) {
await page.getByText("Components").first().isVisible();
} else {
await page.getByText("MCP Server").first().isVisible();
}
await page.getByTestId("add-folder-button").click();
await page.getByTestId("add-project-button").click();
await page
.locator("[data-testid='folder-sidebar']")
.getByText("New Folder")
.locator("[data-testid='project-sidebar']")
.getByText("New Project")
.last()
.isVisible();
await page
.locator("[data-testid='folder-sidebar']")
.getByText("New Folder")
.locator("[data-testid='project-sidebar']")
.getByText("New Project")
.last()
.dblclick();
await page.getByTestId("input-folder").fill("new folder test name");
await page.getByTestId("input-project").fill("new project test name");
await page.keyboard.press("Enter");
await page.getByText("new folder test name").last().isVisible();
await page.getByText("new project test name").last().isVisible();
await page.getByText("My Projects").last().click();
await page.getByText("Starter Project").last().click();
await page.getByText("Basic Prompting").first().hover();
await page.mouse.down();
await page.getByText("test").first().hover();

View file

@ -31,15 +31,15 @@ test(
await page.getByText("Save").last().click();
await page.getByTestId("icon-ChevronLeft").last().click();
await page.getByTestId("add-folder-button").click();
await page.getByTestId("add-project-button").click();
let countFolders = await page.getByText("New Folder").count();
let countFolders = await page.getByText("New Project").count();
while (countFolders > 1) {
await page.getByText("New Folder").first().hover();
await page.getByText("New Project").first().hover();
await page.getByTestId("more-options-button").first().click();
await page.getByTestId("btn-delete-folder").click();
await page.getByTestId("btn-delete-project").click();
await page.getByText("Delete").last().click();
countFolders--;
await page.waitForTimeout(1000);
@ -47,7 +47,7 @@ test(
// Get the bounding boxes of the elements
const sourceElement = await page.getByTestId(`card-${randomName}`).first();
const targetElement = await page.getByText("New Folder").last();
const targetElement = await page.getByText("New Project").last();
const sourceBox = await sourceElement.boundingBox();
const targetBox = await targetElement.boundingBox();
@ -66,7 +66,7 @@ test(
await page.waitForTimeout(3000);
await page.getByText("New Folder").last().click();
await page.getByText("New Project").last().click();
expect(await page.getByTestId(`card-${randomName}`).first().isVisible());
@ -80,7 +80,7 @@ test(
await page.waitForTimeout(3000);
await page.getByText("New Folder").last().click();
await page.getByText("New Project").last().click();
expect(
await page.getByTestId(`card-${secondRandomName}`).first().isVisible(),
);
@ -89,7 +89,7 @@ test(
const secondSourceElement = await page
.getByTestId(`card-${secondRandomName}`)
.first();
const secondTargetElement = await page.getByText("New Folder").last();
const secondTargetElement = await page.getByText("New Project").last();
const secondSourceBox = await secondSourceElement.boundingBox();
const secondTargetBox = await secondTargetElement.boundingBox();
@ -108,7 +108,7 @@ test(
await page.waitForTimeout(3000);
await page.getByText("My Projects").last().click();
await page.getByText("Starter Project").last().click();
expect(
await page.getByTestId(`card-${secondRandomName}`).first().isVisible(),

View file

@ -46,12 +46,16 @@ test(
timeout: 10000,
});
await page.getByText("Components", { exact: true }).click();
await page.getByTestId("home-dropdown-menu").nth(0).click();
await page.getByTestId("btn-download-json").last().click();
await expect(page.getByText(/.*exported successfully/).last()).toBeVisible({
timeout: 10000,
});
if (await page.getByText("Components").first().isVisible()) {
await page.getByText("Components", { exact: true }).click();
await page.getByTestId("home-dropdown-menu").nth(0).click();
await page.getByTestId("btn-download-json").last().click();
await expect(
page.getByText(/.*exported successfully/).last(),
).toBeVisible({
timeout: 10000,
});
}
},
);
@ -69,7 +73,7 @@ test(
if (countEmptyButton > 0) {
await addFlowToTestOnEmptyLangflow(page);
}
await page.getByTestId("upload-folder-button").last().click();
await page.getByTestId("upload-project-button").last().click();
},
);

View file

@ -35,18 +35,20 @@ test(
timeout: 100000,
});
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByText("Components").first().click();
await page.getByText("Basic RAG").first().isVisible();
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
timeout: 100000,
});
await page.getByTestId("home-dropdown-menu").first().click();
await page.getByTestId("icon-Trash2").click();
await page
.getByText("Are you sure you want to delete the selected component?")
.isVisible();
await page.getByText("Delete").nth(1).click();
await page.waitForTimeout(1000);
await page.getByText("Successfully").first().isVisible();
if (await page.getByText("Components").first().isVisible()) {
await page.getByText("Components").first().click();
await page.getByText("Basic RAG").first().isVisible();
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
timeout: 100000,
});
await page.getByTestId("home-dropdown-menu").first().click();
await page.getByTestId("icon-Trash2").click();
await page
.getByText("Are you sure you want to delete the selected component?")
.isVisible();
await page.getByText("Delete").nth(1).click();
await page.waitForTimeout(1000);
await page.getByText("Successfully").first().isVisible();
}
},
);

View file

@ -22,7 +22,7 @@ test(
await page.getByTestId("icon-ChevronLeft").first().click();
await page.waitForSelector("text=my projects", {
await page.waitForSelector("text=starter project", {
timeout: 5000,
});
@ -76,7 +76,7 @@ test(
await page.getByTestId("icon-ChevronLeft").first().click();
await page.waitForSelector("text=my projects", {
await page.waitForSelector("text=starter project", {
timeout: 5000,
});

View file

@ -13,7 +13,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
const firstRunLangflow = await page
.getByTestId("empty-folder-description")
.getByTestId("empty-project-description")
.count();
if (firstRunLangflow > 0) {
@ -58,7 +58,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
const firstRunLangflow = await page
.getByTestId("empty-folder-description")
.getByTestId("empty-project-description")
.count();
if (firstRunLangflow > 0) {
@ -101,7 +101,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
const firstRunLangflow = await page
.getByTestId("empty-folder-description")
.getByTestId("empty-project-description")
.count();
if (firstRunLangflow > 0) {
@ -169,7 +169,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
const firstRunLangflow = await page
.getByTestId("empty-folder-description")
.getByTestId("empty-project-description")
.count();
if (firstRunLangflow > 0) {
@ -241,7 +241,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
const firstRunLangflow = await page
.getByTestId("empty-folder-description")
.getByTestId("empty-project-description")
.count();
if (firstRunLangflow > 0) {

View file

@ -0,0 +1,222 @@
import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { zoomOut } from "../../utils/zoom-out";
test(
"user should be able to manage MCP server actions and configuration",
{ tag: ["@release", "@workspace", "@components"] },
async ({ page }) => {
await awaitBootstrapTest(page);
// Create a new flow
await page.getByTestId("blank-flow").click();
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("api request");
await page.waitForSelector('[data-testid="dataAPI Request"]', {
timeout: 3000,
});
await page
.getByTestId("dataAPI Request")
.hover()
.then(async () => {
await page.getByTestId("add-component-button-api-request").click();
});
await page.waitForSelector(
'[data-testid="generic-node-title-arrangement"]',
{
timeout: 3000,
},
);
await page.getByTestId("generic-node-title-arrangement").click();
// Exit the flow
await page.getByTestId("icon-ChevronLeft").last().click();
// Navigate to MCP server tab
await page.getByTestId("mcp-btn").click();
// Verify MCP server tab is visible
await expect(page.getByTestId("mcp-server-title")).toBeVisible();
await expect(page.getByText("Flows/Actions")).toBeVisible();
// Click on Edit Actions button
await page.getByTestId("button_open_actions").click();
await page.waitForTimeout(500);
// Verify actions modal is open
await expect(page.getByText("MCP Server Actions")).toBeVisible();
// Select some actions
const rowsCount = await page.getByRole("row").count();
expect(rowsCount).toBeGreaterThan(0);
const cellsCount = await page.getByRole("gridcell").count();
expect(cellsCount).toBeGreaterThan(0);
await page.getByRole("gridcell").first().click();
await page.waitForTimeout(500);
const isChecked = await page
.locator('input[data-ref="eInput"]')
.first()
.isChecked();
if (!isChecked) {
await page.locator('input[data-ref="eInput"]').first().click();
await page.waitForTimeout(500);
}
const isCheckedAgain = await page
.locator('input[data-ref="eInput"]')
.first()
.isChecked();
if (isCheckedAgain) {
await page.locator('input[data-ref="eInput"]').first().click();
await page.waitForTimeout(500);
}
const isCheckedAgainAgain = await page
.locator('input[data-ref="eInput"]')
.first()
.isChecked();
expect(isCheckedAgainAgain).toBeFalsy();
// Select first action
await page
.locator('input[data-ref="eInput"]')
.nth(rowsCount + 1)
.click();
await page.waitForTimeout(500);
await page
.getByRole("gridcell")
.nth(cellsCount - 1)
.click();
await page.waitForTimeout(500);
const isLastChecked = await page
.locator('input[data-ref="eInput"]')
.nth(rowsCount + 1)
.isChecked();
expect(isLastChecked).toBeTruthy();
expect(
await page.locator('[data-testid="input_update_name"]').isVisible(),
).toBe(true);
await page.getByTestId("input_update_name").fill("mcp test name");
await page.waitForTimeout(500);
// Close the modal
await page.getByText("Close").last().click();
await page.waitForTimeout(500);
// Verify the selected action is visible in the tab
await expect(page.getByTestId("div-mcp-server-tools")).toBeVisible();
// Generate API key if not in auto login mode
const isAutoLogin = await page.getByText("Generate API key").isVisible();
if (isAutoLogin) {
await page.getByText("Generate API key").click();
await expect(page.getByText("API key generated")).toBeVisible();
}
// Copy configuration
await page.getByTestId("icon-copy").click();
await expect(page.getByTestId("icon-check")).toBeVisible();
// Get the SSE URL from the configuration
const configJson = await page.locator("pre").textContent();
expect(configJson).toContain("mcpServers");
expect(configJson).toContain("supergateway");
expect(configJson).toContain("sse");
// Extract the SSE URL from the configuration
const sseUrlMatch = configJson?.match(
/"args":\s*\[\s*"-y",\s*"supergateway",\s*"--sse",\s*"([^"]+)"/,
);
expect(sseUrlMatch).not.toBeNull();
const sseUrl = sseUrlMatch![1];
// Verify setup guide link
await expect(page.getByText("setup guide")).toBeVisible();
await expect(page.getByText("setup guide")).toHaveAttribute(
"href",
"https://docs.langflow.org/mcp-server#connect-clients-to-use-the-servers-actions",
);
await awaitBootstrapTest(page);
// 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 server");
await page.waitForSelector('[data-testid="toolsMCP Server"]', {
timeout: 30000,
});
await page
.getByTestId("toolsMCP Server")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
targetPosition: { x: 0, y: 0 },
});
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 page.waitForSelector('[data-testid="textarea_str_sse_url"]', {
state: "visible",
timeout: 30000,
});
await page.getByTestId("textarea_str_sse_url").fill(sseUrl);
await page.waitForTimeout(2000);
// Wait for the tools to become available
let attempts = 0;
const maxAttempts = 3;
let dropdownEnabled = false;
while (attempts < maxAttempts && !dropdownEnabled) {
await page.getByTestId("refresh-button-sse_url").click();
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`);
}
}
if (!dropdownEnabled) {
throw new Error(
"Dropdown did not become enabled after multiple refresh attempts",
);
}
// Verify tools are available
await page.getByTestId("dropdown_str_tool").click();
await page.waitForTimeout(2000);
const fetchOptionCount = await page.getByText("mcp_test_name").count();
expect(fetchOptionCount).toBeGreaterThan(0);
},
);

View file

@ -22,13 +22,13 @@ test("user must be able to move flow from folder", async ({ page }) => {
await page.getByText("Changes saved successfully").isVisible();
await page.getByTestId("icon-ChevronLeft").click();
await page.waitForSelector('[data-testid="add-folder-button"]', {
await page.waitForSelector('[data-testid="add-project-button"]', {
timeout: 3000,
});
await page.getByTestId("add-folder-button").click();
await page.getByTestId("add-project-button").click();
//wait for the folder to be created and changed to the new folder
//wait for the project to be created and changed to the new project
await page.waitForTimeout(1000);
await page.getByTestId("sidebar-nav-My Projects").click();
@ -38,12 +38,12 @@ test("user must be able to move flow from folder", async ({ page }) => {
await page
.getByTestId("list-card")
.first()
.dragTo(page.locator('//*[@id="sidebar-nav-New Folder"]'));
.dragTo(page.locator('//*[@id="sidebar-nav-New Project"]'));
//wait for the drag and drop to be completed
await page.waitForTimeout(1000);
await page.getByTestId("sidebar-nav-New Folder").click();
await page.getByTestId("sidebar-nav-New Project").click();
await page.waitForSelector('[data-testid="list-card"]', {
timeout: 3000,

View file

@ -20,9 +20,9 @@ test(
await page.getByText("Close").last().click();
await page.getByTestId("add-folder-button").click();
await page.getByTestId("add-project-button").click();
await page.getByText("New Folder").last().click();
await page.getByText("New Project").last().click();
await page.waitForSelector("text=new flow", { timeout: 30000 });