diff --git a/src/backend/base/langflow/alembic/versions/3162e83e485f_add_auth_settings_to_folder_and_merge.py b/src/backend/base/langflow/alembic/versions/3162e83e485f_add_auth_settings_to_folder_and_merge.py
new file mode 100644
index 000000000000..7e1bfd538594
--- /dev/null
+++ b/src/backend/base/langflow/alembic/versions/3162e83e485f_add_auth_settings_to_folder_and_merge.py
@@ -0,0 +1,58 @@
+"""Add auth_settings column to folder table and merge migration branches.
+
+Revision ID: 3162e83e485f
+Revises: 0ae3a2674f32, d9a6ea21edcd
+Create Date: 2025-01-16 13:00:00.000000
+
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "3162e83e485f"
+down_revision: str | Sequence[str] | None = ("0ae3a2674f32", "d9a6ea21edcd")
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ """Add auth_settings column to folder table and merge migration branches."""
+ conn = op.get_bind()
+ inspector = sa.inspect(conn)
+
+ # Check if folder table exists
+ table_names = inspector.get_table_names()
+ if "folder" not in table_names:
+ # If folder table doesn't exist, skip this migration
+ return
+
+ # Get current column names in folder table
+ column_names = [column["name"] for column in inspector.get_columns("folder")]
+
+ # Add auth_settings column to folder table if it doesn't exist
+ with op.batch_alter_table("folder", schema=None) as batch_op:
+ if "auth_settings" not in column_names:
+ batch_op.add_column(sa.Column("auth_settings", sa.JSON(), nullable=True))
+
+
+def downgrade() -> None:
+ """Remove auth_settings column from folder table."""
+ conn = op.get_bind()
+ inspector = sa.inspect(conn)
+
+ # Check if folder table exists
+ table_names = inspector.get_table_names()
+ if "folder" not in table_names:
+ # If folder table doesn't exist, skip this migration
+ return
+
+ # Get current column names in folder table
+ column_names = [column["name"] for column in inspector.get_columns("folder")]
+
+ # Remove auth_settings column from folder table if it exists
+ with op.batch_alter_table("folder", schema=None) as batch_op:
+ if "auth_settings" in column_names:
+ batch_op.drop_column("auth_settings")
diff --git a/src/backend/base/langflow/api/v1/mcp_projects.py b/src/backend/base/langflow/api/v1/mcp_projects.py
index 0c313f38f411..832f76c0f9e8 100644
--- a/src/backend/base/langflow/api/v1/mcp_projects.py
+++ b/src/backend/base/langflow/api/v1/mcp_projects.py
@@ -29,11 +29,17 @@
handle_mcp_errors,
handle_read_resource,
)
-from langflow.api.v1.schemas import MCPInstallRequest, MCPSettings
+from langflow.api.v1.schemas import (
+ MCPInstallRequest,
+ MCPProjectResponse,
+ MCPProjectUpdateRequest,
+ MCPSettings,
+)
from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH
from langflow.base.mcp.util import sanitize_mcp_name
from langflow.services.database.models import Flow, Folder
from langflow.services.deps import get_settings_service, session_scope
+from langflow.services.settings.feature_flags import FEATURE_FLAGS
logger = logging.getLogger(__name__)
@@ -60,7 +66,7 @@ async def list_project_tools(
current_user: CurrentActiveMCPUser,
*,
mcp_enabled: bool = True,
-) -> list[MCPSettings]:
+) -> MCPProjectResponse:
"""List all tools in a project that are enabled for MCP."""
tools: list[MCPSettings] = []
try:
@@ -114,12 +120,19 @@ async def list_project_tools(
logger.warning(msg)
continue
+ # Get project-level auth settings
+ auth_settings = None
+ if project.auth_settings:
+ from langflow.api.v1.schemas import AuthSettings
+
+ auth_settings = AuthSettings(**project.auth_settings)
+
except Exception as e:
msg = f"Error listing project tools: {e!s}"
logger.exception(msg)
raise HTTPException(status_code=500, detail=str(e)) from e
- return tools
+ return MCPProjectResponse(tools=tools, auth_settings=auth_settings)
@router.head("/{project_id}/sse", response_class=HTMLResponse, include_in_schema=False)
@@ -218,10 +231,10 @@ async def handle_project_messages_with_slash(project_id: UUID, request: Request,
@router.patch("/{project_id}", status_code=200)
async def update_project_mcp_settings(
project_id: UUID,
- settings: list[MCPSettings],
+ request: MCPProjectUpdateRequest,
current_user: CurrentActiveMCPUser,
):
- """Update the MCP settings of all flows in a project."""
+ """Update the MCP settings of all flows in a project and project-level auth settings."""
try:
async with session_scope() as session:
# Fetch the project first to verify it exists and belongs to the current user
@@ -236,9 +249,16 @@ async def update_project_mcp_settings(
if not project:
raise HTTPException(status_code=404, detail="Project not found")
+ # Update project-level auth settings
+ if request.auth_settings:
+ project.auth_settings = request.auth_settings.model_dump(mode="json")
+ else:
+ project.auth_settings = None
+ session.add(project)
+
# Query flows in the project
flows = (await session.exec(select(Flow).where(Flow.folder_id == project_id))).all()
- flows_to_update = {x.id: x for x in settings}
+ flows_to_update = {x.id: x for x in request.settings}
updated_flows = []
for flow in flows:
@@ -256,7 +276,7 @@ async def update_project_mcp_settings(
await session.commit()
- return {"message": f"Updated MCP settings for {len(updated_flows)} flows"}
+ return {"message": f"Updated MCP settings for {len(updated_flows)} flows and project auth settings"}
except Exception as e:
msg = f"Error updating project MCP settings: {e!s}"
@@ -348,7 +368,8 @@ async def install_mcp_config(
# Determine command and args based on operating system
os_type = platform.system()
command = "uvx"
- args = ["mcp-proxy", sse_url]
+ mcp_tool = "mcp-composer" if FEATURE_FLAGS.mcp_composer else "mcp-proxy"
+ args = [mcp_tool, sse_url]
# Check if running on WSL (will appear as Linux but with Microsoft in release info)
is_wsl = os_type == "Linux" and "microsoft" in platform.uname().release.lower()
@@ -381,7 +402,7 @@ async def install_mcp_config(
if os_type == "Windows":
command = "cmd"
- args = ["/c", "uvx", "mcp-proxy", sse_url]
+ args = ["/c", "uvx", mcp_tool, sse_url]
logger.debug("Windows detected, using cmd command")
name = project.name
diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py
index 4229dfd69036..bd55ddf82d9e 100644
--- a/src/backend/base/langflow/api/v1/schemas.py
+++ b/src/backend/base/langflow/api/v1/schemas.py
@@ -8,6 +8,7 @@
BaseModel,
ConfigDict,
Field,
+ SecretStr,
field_serializer,
field_validator,
model_serializer,
@@ -440,6 +441,27 @@ class CancelFlowResponse(BaseModel):
message: str
+class AuthSettings(BaseModel):
+ """Model representing authentication settings for MCP."""
+
+ auth_type: Literal["none", "apikey", "basic", "bearer", "iam", "oauth"] = "none"
+ api_key: SecretStr | None = None
+ username: str | None = None
+ password: SecretStr | None = None
+ bearer_token: SecretStr | None = None
+ iam_endpoint: str | None = None
+ oauth_host: str | None = None
+ oauth_port: str | None = None
+ oauth_server_url: str | None = None
+ oauth_callback_path: str | None = None
+ oauth_client_id: str | None = None
+ oauth_client_secret: str | None = None
+ oauth_auth_url: str | None = None
+ oauth_token_url: str | None = None
+ oauth_mcp_scope: str | None = None
+ oauth_provider_scope: str | None = None
+
+
class MCPSettings(BaseModel):
"""Model representing MCP settings for a flow."""
@@ -451,5 +473,19 @@ class MCPSettings(BaseModel):
description: str | None = None
+class MCPProjectUpdateRequest(BaseModel):
+ """Request model for updating MCP project settings including auth."""
+
+ settings: list[MCPSettings]
+ auth_settings: AuthSettings | None = None
+
+
+class MCPProjectResponse(BaseModel):
+ """Response model for MCP project tools with auth settings."""
+
+ tools: list[MCPSettings]
+ auth_settings: AuthSettings | None = None
+
+
class MCPInstallRequest(BaseModel):
client: str
diff --git a/src/backend/base/langflow/services/database/models/folder/model.py b/src/backend/base/langflow/services/database/models/folder/model.py
index 82a2e6d56528..93949c48417e 100644
--- a/src/backend/base/langflow/services/database/models/folder/model.py
+++ b/src/backend/base/langflow/services/database/models/folder/model.py
@@ -2,7 +2,7 @@
from uuid import UUID, uuid4
from sqlalchemy import Text, UniqueConstraint
-from sqlmodel import Column, Field, Relationship, SQLModel
+from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.services.database.models.flow.model import Flow, FlowRead
from langflow.services.database.models.user.model import User
@@ -11,6 +11,11 @@
class FolderBase(SQLModel):
name: str = Field(index=True)
description: str | None = Field(default=None, sa_column=Column(Text))
+ auth_settings: dict | None = Field(
+ default=None,
+ sa_column=Column(JSON, nullable=True),
+ description="Authentication settings for the folder/project",
+ )
class Folder(FolderBase, table=True): # type: ignore[call-arg]
@@ -53,3 +58,4 @@ class FolderUpdate(SQLModel):
parent_id: UUID | None = None
components: list[UUID] = Field(default_factory=list)
flows: list[UUID] = Field(default_factory=list)
+ auth_settings: dict | None = None
diff --git a/src/backend/base/langflow/services/settings/feature_flags.py b/src/backend/base/langflow/services/settings/feature_flags.py
index a234710d41a0..d543df5a17f8 100644
--- a/src/backend/base/langflow/services/settings/feature_flags.py
+++ b/src/backend/base/langflow/services/settings/feature_flags.py
@@ -3,6 +3,7 @@
class FeatureFlags(BaseSettings):
mvp_components: bool = False
+ mcp_composer: bool = True
class Config:
env_prefix = "LANGFLOW_FEATURE_"
diff --git a/src/backend/tests/unit/api/v1/test_mcp_projects.py b/src/backend/tests/unit/api/v1/test_mcp_projects.py
index 5de5c6321cd2..8e44ad4bbfba 100644
--- a/src/backend/tests/unit/api/v1/test_mcp_projects.py
+++ b/src/backend/tests/unit/api/v1/test_mcp_projects.py
@@ -206,20 +206,30 @@ async def test_update_project_mcp_settings_success(
):
"""Test successful update of MCP settings using real database."""
# Create settings for updating the flow
- settings = [
- {
- "id": str(test_flow_for_update.id),
- "action_name": "updated_action",
- "action_description": "Updated description",
- "mcp_enabled": False,
- "name": test_flow_for_update.name,
- "description": test_flow_for_update.description,
- }
- ]
+ json_payload = {
+ "settings": [
+ {
+ "id": str(test_flow_for_update.id),
+ "action_name": "updated_action",
+ "action_description": "Updated description",
+ "mcp_enabled": False,
+ "name": test_flow_for_update.name,
+ "description": test_flow_for_update.description,
+ }
+ ],
+ "auth_settings": {
+ "auth_type": "none",
+ "api_key": None,
+ "iam_endpoint": None,
+ "username": None,
+ "password": None,
+ "bearer_token": None,
+ },
+ }
# Make the real PATCH request
response = await client.patch(
- f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=settings
+ f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Assert response
@@ -268,11 +278,21 @@ async def test_update_project_mcp_settings_empty_settings(client: AsyncClient, u
# Use real database objects instead of mocks to avoid the coroutine issue
# Empty settings list
- settings: list = []
+ json_payload = {
+ "settings": [],
+ "auth_settings": {
+ "auth_type": "none",
+ "api_key": None,
+ "iam_endpoint": None,
+ "username": None,
+ "password": None,
+ "bearer_token": None,
+ },
+ }
# Make the request to the actual endpoint
response = await client.patch(
- f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=settings
+ f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Verify response - the real endpoint should handle empty settings correctly
@@ -385,20 +405,30 @@ async def test_user_can_update_own_flow_mcp_settings(
):
"""Test that a user can update MCP settings for their own flows using real database."""
# User attempts to update their own flow settings
- updated_settings = [
- {
- "id": str(user_test_flow.id),
- "action_name": "updated_user_action",
- "action_description": "Updated user action description",
- "mcp_enabled": False,
- "name": "User Test Flow",
- "description": "This flow belongs to the active user",
- }
- ]
+ json_payload = {
+ "settings": [
+ {
+ "id": str(user_test_flow.id),
+ "action_name": "updated_user_action",
+ "action_description": "Updated user action description",
+ "mcp_enabled": False,
+ "name": "User Test Flow",
+ "description": "This flow belongs to the active user",
+ }
+ ],
+ "auth_settings": {
+ "auth_type": "none",
+ "api_key": None,
+ "iam_endpoint": None,
+ "username": None,
+ "password": None,
+ "bearer_token": None,
+ },
+ }
# Make the PATCH request to update settings
response = await client.patch(
- f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=updated_settings
+ f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Should succeed as the user owns this project and flow
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 9a424fe19e40..c3717f816b51 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -20,6 +20,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.2.1",
@@ -5048,6 +5049,38 @@
}
}
},
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz",
+ "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
diff --git a/src/frontend/package.json b/src/frontend/package.json
index 84f4355562b5..985b6ed80517 100644
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -15,6 +15,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.2.1",
diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx
index 5fcbe726f916..bcd3fc343800 100644
--- a/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx
+++ b/src/frontend/src/components/core/parameterRenderComponent/components/ToolsComponent/index.tsx
@@ -1,5 +1,7 @@
import { useState } from "react";
+import ShadTooltip from "@/components/common/shadTooltipComponent";
import { ICON_STROKE_WIDTH } from "@/constants/constants";
+import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags";
import ToolsModal from "@/modals/toolsModal";
import { cn, testIdCase } from "@/utils/utils";
import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
@@ -59,28 +61,47 @@ export default function ToolsComponent({
icon={icon}
/>
- {(visibleActions.length > 0 || isAction) && (
-
- )}
+
+
+
+
+ Flows/Tools
+
+
+
+
+ {(visibleActions.length > 0 || isAction) && (
+
+ )}
+
{!value ? (
{[...Array(4)].map((_, index) => (
diff --git a/src/frontend/src/components/ui/radio-group.tsx b/src/frontend/src/components/ui/radio-group.tsx
new file mode 100644
index 000000000000..3d1156dda801
--- /dev/null
+++ b/src/frontend/src/components/ui/radio-group.tsx
@@ -0,0 +1,40 @@
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
+import { CircleIcon } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/utils/utils";
+
+function RadioGroup({
+ className,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+ );
+}
+function RadioGroupItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ );
+}
+export { RadioGroup, RadioGroupItem };
diff --git a/src/frontend/src/controllers/API/queries/mcp/use-get-flows-mcp.ts b/src/frontend/src/controllers/API/queries/mcp/use-get-flows-mcp.ts
index 939e33e5f3fd..750686bb415e 100644
--- a/src/frontend/src/controllers/API/queries/mcp/use-get-flows-mcp.ts
+++ b/src/frontend/src/controllers/API/queries/mcp/use-get-flows-mcp.ts
@@ -1,5 +1,5 @@
import type { useQueryFunctionType } from "@/types/api";
-import type { MCPSettingsType } from "@/types/mcp";
+import type { MCPProjectResponseType } from "@/types/mcp";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
@@ -8,7 +8,7 @@ interface IGetFlowsMCP {
projectId: string;
}
-type getFlowsMCPResponse = Array;
+type getFlowsMCPResponse = MCPProjectResponseType;
export const useGetFlowsMCP: useQueryFunctionType<
IGetFlowsMCP,
@@ -24,7 +24,7 @@ export const useGetFlowsMCP: useQueryFunctionType<
return data;
} catch (error) {
console.error(error);
- return [];
+ return { tools: [], auth_settings: undefined };
}
};
diff --git a/src/frontend/src/controllers/API/queries/mcp/use-patch-flows-mcp.ts b/src/frontend/src/controllers/API/queries/mcp/use-patch-flows-mcp.ts
index a4457f7e142e..ed9c2df756cf 100644
--- a/src/frontend/src/controllers/API/queries/mcp/use-patch-flows-mcp.ts
+++ b/src/frontend/src/controllers/API/queries/mcp/use-patch-flows-mcp.ts
@@ -1,6 +1,6 @@
import type { UseMutationResult } from "@tanstack/react-query";
import type { useMutationFunctionType } from "@/types/api";
-import type { MCPSettingsType } from "@/types/mcp";
+import type { AuthSettingsType, MCPSettingsType } from "@/types/mcp";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
@@ -9,21 +9,26 @@ interface PatchFlowMCPParams {
project_id: string;
}
+interface PatchFlowMCPRequest {
+ settings: MCPSettingsType[];
+ auth_settings?: AuthSettingsType;
+}
+
interface PatchFlowMCPResponse {
message: string;
}
export const usePatchFlowsMCP: useMutationFunctionType<
PatchFlowMCPParams,
- MCPSettingsType[],
+ PatchFlowMCPRequest,
PatchFlowMCPResponse
> = (params, options?) => {
const { mutate, queryClient } = UseRequestProcessor();
- async function patchFlowMCP(flowMCP: MCPSettingsType[]): Promise {
+ async function patchFlowMCP(requestData: PatchFlowMCPRequest): Promise {
const res = await api.patch(
`${getURL("MCP")}/${params.project_id}`,
- flowMCP,
+ requestData,
);
return res.data.message;
}
@@ -31,7 +36,7 @@ export const usePatchFlowsMCP: useMutationFunctionType<
const mutation: UseMutationResult<
PatchFlowMCPResponse,
any,
- MCPSettingsType[]
+ PatchFlowMCPRequest
> = mutate(["usePatchFlowsMCP"], patchFlowMCP, {
onSettled: () => {
queryClient.refetchQueries({ queryKey: ["useGetFlowsMCP"] });
diff --git a/src/frontend/src/customization/feature-flags.ts b/src/frontend/src/customization/feature-flags.ts
index 725c8baee491..79c18b31b51d 100644
--- a/src/frontend/src/customization/feature-flags.ts
+++ b/src/frontend/src/customization/feature-flags.ts
@@ -15,3 +15,5 @@ export const ENABLE_VOICE_ASSISTANT = true;
export const ENABLE_IMAGE_ON_PLAYGROUND = false;
export const ENABLE_MCP = true;
export const ENABLE_MCP_NOTICE = false;
+export const ENABLE_MCP_COMPOSER =
+ process.env.LANGFLOW_FEATURE_MCP_COMPOSER === "true";
diff --git a/src/frontend/src/modals/authModal/index.tsx b/src/frontend/src/modals/authModal/index.tsx
new file mode 100644
index 000000000000..45ce08a46bc9
--- /dev/null
+++ b/src/frontend/src/modals/authModal/index.tsx
@@ -0,0 +1,468 @@
+import { useEffect, useState } from "react";
+import ForwardedIconComponent from "@/components/common/genericIconComponent";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Separator } from "@/components/ui/separator";
+import type { AuthSettingsType } from "@/types/mcp";
+import { AUTH_METHODS_ARRAY } from "@/utils/mcpUtils";
+import BaseModal from "../baseModal";
+
+interface AuthModalProps {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ authSettings?: AuthSettingsType;
+ onSave: (authSettings: AuthSettingsType) => void;
+}
+
+const AuthModal = ({ open, setOpen, authSettings, onSave }: AuthModalProps) => {
+ const [authType, setAuthType] = useState(
+ authSettings?.auth_type || "none",
+ );
+ const [authFields, setAuthFields] = useState<{
+ apiKey?: string;
+ iamEndpoint?: string;
+ username?: string;
+ password?: string;
+ bearerToken?: string;
+ oauthHost?: string;
+ oauthPort?: string;
+ oauthServerUrl?: string;
+ oauthCallbackPath?: string;
+ oauthClientId?: string;
+ oauthClientSecret?: string;
+ oauthAuthUrl?: string;
+ oauthTokenUrl?: string;
+ oauthMcpScope?: string;
+ oauthProviderScope?: string;
+ }>({
+ apiKey: authSettings?.api_key || "",
+ iamEndpoint: authSettings?.iam_endpoint || "",
+ username: authSettings?.username || "",
+ password: authSettings?.password || "",
+ bearerToken: authSettings?.bearer_token || "",
+ oauthHost: authSettings?.oauth_host || "",
+ oauthPort: authSettings?.oauth_port || "",
+ oauthServerUrl: authSettings?.oauth_server_url || "",
+ oauthCallbackPath: authSettings?.oauth_callback_path || "",
+ oauthClientId: authSettings?.oauth_client_id || "",
+ oauthClientSecret: authSettings?.oauth_client_secret || "",
+ oauthAuthUrl: authSettings?.oauth_auth_url || "",
+ oauthTokenUrl: authSettings?.oauth_token_url || "",
+ oauthMcpScope: authSettings?.oauth_mcp_scope || "",
+ oauthProviderScope: authSettings?.oauth_provider_scope || "",
+ });
+
+ // Update auth state when authSettings prop changes
+ useEffect(() => {
+ if (authSettings) {
+ setAuthType(authSettings.auth_type || "none");
+ setAuthFields({
+ apiKey: authSettings.api_key || "",
+ iamEndpoint: authSettings.iam_endpoint || "",
+ username: authSettings.username || "",
+ password: authSettings.password || "",
+ bearerToken: authSettings.bearer_token || "",
+ oauthHost: authSettings.oauth_host || "",
+ oauthPort: authSettings.oauth_port || "",
+ oauthServerUrl: authSettings.oauth_server_url || "",
+ oauthCallbackPath: authSettings.oauth_callback_path || "",
+ oauthClientId: authSettings.oauth_client_id || "",
+ oauthClientSecret: authSettings.oauth_client_secret || "",
+ oauthAuthUrl: authSettings.oauth_auth_url || "",
+ oauthTokenUrl: authSettings.oauth_token_url || "",
+ oauthMcpScope: authSettings.oauth_mcp_scope || "",
+ oauthProviderScope: authSettings.oauth_provider_scope || "",
+ });
+ }
+ }, [authSettings]);
+
+ const handleAuthTypeChange = (value: string) => {
+ setAuthType(value);
+ setAuthFields({});
+ };
+
+ const handleAuthFieldChange = (field: string, value: string) => {
+ setAuthFields((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSave = () => {
+ const authSettingsToSave: AuthSettingsType = {
+ auth_type: authType,
+ ...(authType === "apikey" && { api_key: authFields.apiKey }),
+ ...(authType === "basic" && {
+ username: authFields.username,
+ password: authFields.password,
+ }),
+ ...(authType === "iam" && {
+ iam_endpoint: authFields.iamEndpoint,
+ api_key: authFields.apiKey,
+ }),
+ ...(authType === "bearer" && { bearer_token: authFields.bearerToken }),
+ ...(authType === "oauth" && {
+ oauth_host: authFields.oauthHost,
+ oauth_port: authFields.oauthPort,
+ oauth_server_url: authFields.oauthServerUrl,
+ oauth_callback_path: authFields.oauthCallbackPath,
+ oauth_client_id: authFields.oauthClientId,
+ oauth_client_secret: authFields.oauthClientSecret,
+ oauth_auth_url: authFields.oauthAuthUrl,
+ oauth_token_url: authFields.oauthTokenUrl,
+ oauth_mcp_scope: authFields.oauthMcpScope,
+ oauth_provider_scope: authFields.oauthProviderScope,
+ }),
+ };
+
+ onSave(authSettingsToSave);
+ setOpen(false);
+ };
+
+ return (
+
+
+ Authentication
+
+
+
+ {/* Left column - Radio buttons */}
+
+
+ {AUTH_METHODS_ARRAY.map((option) => (
+
+
+
+
+ ))}
+
+
+ {authType !== "none" &&
}
+
+ {/* Right column - Input fields */}
+ {authType !== "none" && (
+
+ {authType === "apikey" && (
+
+
+
+ handleAuthFieldChange("apiKey", e.target.value)
+ }
+ />
+
+ )}
+
+ {authType === "basic" && (
+
+ )}
+
+ {authType === "bearer" && (
+
+
+
+ handleAuthFieldChange("bearerToken", e.target.value)
+ }
+ />
+
+ )}
+
+ {authType === "iam" && (
+
+ )}
+
+ {authType === "oauth" && (
+
+
+
+
+
+ handleAuthFieldChange("oauthServerUrl", e.target.value)
+ }
+ />
+
+
+
+
+ handleAuthFieldChange(
+ "oauthCallbackPath",
+ e.target.value,
+ )
+ }
+ />
+
+
+
+
+
+ handleAuthFieldChange("oauthAuthUrl", e.target.value)
+ }
+ />
+
+
+
+
+ handleAuthFieldChange("oauthTokenUrl", e.target.value)
+ }
+ />
+
+
+
+ )}
+
+ )}
+
+
+
+
+ );
+};
+
+export default AuthModal;
diff --git a/src/frontend/src/modals/toolsModal/index.tsx b/src/frontend/src/modals/toolsModal/index.tsx
index d39581820316..39def4fe8567 100644
--- a/src/frontend/src/modals/toolsModal/index.tsx
+++ b/src/frontend/src/modals/toolsModal/index.tsx
@@ -73,18 +73,20 @@ const ToolsModal = forwardRef(
-
-
-
-
+
diff --git a/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx b/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx
index e992a030290f..f6cc832cc8ff 100644
--- a/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx
+++ b/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx
@@ -14,13 +14,16 @@ import {
} from "@/controllers/API/queries/mcp";
import { useGetInstalledMCP } from "@/controllers/API/queries/mcp/use-get-installed-mcp";
import { usePatchInstallMCP } from "@/controllers/API/queries/mcp/use-patch-install-mcp";
+import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags";
import { useCustomIsLocalConnection } from "@/customization/hooks/use-custom-is-local-connection";
import useTheme from "@/customization/hooks/use-custom-theme";
import { customGetMCPUrl } from "@/customization/utils/custom-mcp-url";
+import AuthModal from "@/modals/authModal";
import useAlertStore from "@/stores/alertStore";
import useAuthStore from "@/stores/authStore";
import { useFolderStore } from "@/stores/foldersStore";
-import type { MCPSettingsType } from "@/types/mcp";
+import type { AuthSettingsType, MCPSettingsType } from "@/types/mcp";
+import { AUTH_METHODS } from "@/utils/mcpUtils";
import { parseString } from "@/utils/stringManipulation";
import { cn, getOS } from "@/utils/utils";
@@ -131,11 +134,16 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const [isCopied, setIsCopied] = useState(false);
const [apiKey, setApiKey] = useState
("");
const [isGeneratingApiKey, setIsGeneratingApiKey] = useState(false);
+ const [authModalOpen, setAuthModalOpen] = useState(false);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
- const { data: flowsMCP } = useGetFlowsMCP({ projectId });
+ const { data: mcpProjectData } = useGetFlowsMCP({ projectId });
const { mutate: patchFlowsMCP } = usePatchFlowsMCP({ project_id: projectId });
+
+ // Extract tools and auth_settings from the response
+ const flowsMCP = mcpProjectData?.tools || [];
+ const currentAuthSettings = mcpProjectData?.auth_settings;
const { mutate: patchInstallMCP } = usePatchInstallMCP({
project_id: projectId,
});
@@ -156,14 +164,44 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
isLocalConnection ? "Auto install" : "JSON",
);
- const handleOnNewValue = (value) => {
- const flowsMCPData: MCPSettingsType[] = value.value.map((flow) => ({
+ const handleOnNewValue = (value: any) => {
+ const flowsMCPData: MCPSettingsType[] = value.value.map((flow: any) => ({
id: flow.id,
action_name: flow.name,
action_description: flow.description,
mcp_enabled: flow.status,
}));
- patchFlowsMCP(flowsMCPData);
+
+ // Prepare the request with both settings and auth_settings
+ // If ENABLE_MCP_COMPOSER is false, always use "none" for auth_type
+ const finalAuthSettings = ENABLE_MCP_COMPOSER
+ ? currentAuthSettings
+ : { auth_type: "none" };
+
+ const requestData = {
+ settings: flowsMCPData,
+ auth_settings: finalAuthSettings,
+ };
+
+ patchFlowsMCP(requestData);
+ };
+
+ const handleAuthSave = (authSettings: AuthSettingsType) => {
+ // Update the current flows with the new auth settings
+ const flowsMCPData: MCPSettingsType[] =
+ flowsMCP?.map((flow) => ({
+ id: flow.id,
+ action_name: flow.action_name,
+ action_description: flow.action_description,
+ mcp_enabled: flow.mcp_enabled,
+ })) || [];
+
+ const requestData = {
+ settings: flowsMCPData,
+ auth_settings: authSettings,
+ };
+
+ patchFlowsMCP(requestData);
};
const flowsMCPData = flowsMCP?.map((flow) => ({
@@ -187,10 +225,93 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const apiUrl = customGetMCPUrl(projectId);
+ // Generate auth headers based on the authentication type
+ const getAuthHeaders = () => {
+ // If MCP auth is disabled, use the previous API key behavior
+ if (!ENABLE_MCP_COMPOSER) {
+ if (isAutoLogin) return "";
+ return `
+ "--headers",
+ "x-api-key",
+ "${apiKey || "YOUR_API_KEY"}",`;
+ }
+
+ if (!currentAuthSettings || currentAuthSettings.auth_type === "none") {
+ return "";
+ }
+
+ switch (currentAuthSettings.auth_type) {
+ case "apikey":
+ return `
+ "--headers",
+ "x-api-key",
+ "${currentAuthSettings.api_key || "YOUR_API_KEY"}",`;
+ case "basic":
+ return `
+ "--headers",
+ "Authorization",
+ "Basic ${btoa(
+ `${currentAuthSettings.username || "USERNAME"}:${
+ currentAuthSettings.password || "PASSWORD"
+ }`,
+ )}",`;
+ case "bearer":
+ return `
+ "--headers",
+ "Authorization",
+ "Bearer ${currentAuthSettings.bearer_token || "YOUR_BEARER_TOKEN"}",`;
+ case "iam":
+ return `
+ "--headers",
+ "x-api-key",
+ "${currentAuthSettings.api_key || "YOUR_IAM_TOKEN"}",
+ "--headers",
+ "x-iam-endpoint",
+ "${currentAuthSettings.iam_endpoint || "YOUR_IAM_ENDPOINT"}",`;
+ case "oauth":
+ return `
+ "--auth-type",
+ "oauth"`;
+ default:
+ return "";
+ }
+ };
+
+ const getEnvVars = () => {
+ if (!ENABLE_MCP_COMPOSER || currentAuthSettings?.auth_type === "none")
+ return "";
+ if (currentAuthSettings?.auth_type === "oauth") {
+ return `
+ "env": {
+ "OAUTH_HOST": "${currentAuthSettings.oauth_host || "YOUR_OAUTH_HOST"}",
+ "OAUTH_PORT": "${currentAuthSettings.oauth_port || "YOUR_OAUTH_PORT"}",
+ "OAUTH_SERVER_URL": "${currentAuthSettings.oauth_server_url || "YOUR_OAUTH_SERVER_URL"}",
+ "OAUTH_CALLBACK_PATH": "${currentAuthSettings.oauth_callback_path || "YOUR_OAUTH_CALLBACK_PATH"}",
+ "OAUTH_CLIENT_ID": "${currentAuthSettings.oauth_client_id || "YOUR_OAUTH_CLIENT_ID"}",
+ "OAUTH_CLIENT_SECRET": "${currentAuthSettings.oauth_client_secret || "YOUR_OAUTH_CLIENT_SECRET"}",
+ "OAUTH_AUTH_URL": "${currentAuthSettings.oauth_auth_url || "YOUR_OAUTH_AUTH_URL"}",
+ "OAUTH_TOKEN_URL": "${currentAuthSettings.oauth_token_url || "YOUR_OAUTH_TOKEN_URL"}",
+ "OAUTH_MCP_SCOPE": "${currentAuthSettings.oauth_mcp_scope || "YOUR_OAUTH_MCP_SCOPE"}",
+ "OAUTH_PROVIDER_SCOPE": "${currentAuthSettings.oauth_provider_scope || "YOUR_OAUTH_PROVIDER_SCOPE"}",
+ }`;
+ }
+ return "";
+ };
+
const MCP_SERVER_JSON = `{
"mcpServers": {
- "lf-${parseString(folderName ?? "project", ["snake_case", "no_blank", "lowercase"]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
- "command": "${selectedPlatform === "windows" ? "cmd" : selectedPlatform === "wsl" ? "wsl" : "uvx"}",
+ "lf-${parseString(folderName ?? "project", [
+ "snake_case",
+ "no_blank",
+ "lowercase",
+ ]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
+ "command": "${
+ selectedPlatform === "windows"
+ ? "cmd"
+ : selectedPlatform === "wsl"
+ ? "wsl"
+ : "uvx"
+ }",
"args": [
${
selectedPlatform === `windows`
@@ -201,16 +322,9 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
? `"uvx",
`
: ""
- }"mcp-proxy",${
- isAutoLogin
- ? ""
- : `
- "--headers",
- "x-api-key",
- "${apiKey || "YOUR_API_KEY"}",`
- }
+ }"${ENABLE_MCP_COMPOSER ? "mcp-composer" : "mcp-proxy"}",${getAuthHeaders()}
"${apiUrl}"
- ]
+ ]${ENABLE_MCP_COMPOSER && currentAuthSettings?.auth_type === "oauth" ? `,` : ""}${getEnvVars()}
}
}
}`;
@@ -239,7 +353,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
.then((res) => {
setApiKey(res["api_key"]);
})
- .catch((err) => {})
+ .catch(() => {})
.finally(() => {
setIsGeneratingApiKey(false);
});
@@ -247,42 +361,35 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const [loadingMCP, setLoadingMCP] = useState([]);
+ // Check if authentication is configured (not "none")
+ const hasAuthentication =
+ currentAuthSettings?.auth_type && currentAuthSettings.auth_type !== "none";
+
return (
-
- MCP Server
-
-
- Access your Project's flows as Tools within a MCP Server. Learn more in
- our
-
- {" "}
- Projects as MCP Servers guide.
-
+
-
-
-
- Flows/Tools
-
-
-
-
-
-
+ {ENABLE_MCP_COMPOSER && (
+
+
+ Auth:
+ {!hasAuthentication ? (
+
+
+ None (public)
+
+ ) : (
+
+
+ {AUTH_METHODS[
+ currentAuthSettings.auth_type as keyof typeof AUTH_METHODS
+ ]?.label || currentAuthSettings.auth_type}
+
+ )}
+
+
+
+ )}
+
- {[{ name: "Auto install" }, { name: "JSON" }].map(
- (item, index) => (
-
- ),
- )}
+ {[{ name: "Auto install" }, { name: "JSON" }].map((item) => (
+
+ ))}
{selectedMode === "JSON" && (
@@ -325,10 +467,10 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
onValueChange={setSelectedPlatform}
>
- {operatingSystemTabs.map((tab, index) => (
+ {operatingSystemTabs.map((tab) => (
{
)}
{autoInstallers.map((installer) => (
+ {ENABLE_MCP_COMPOSER && (
+
+ )}
);
};
diff --git a/src/frontend/src/types/mcp/index.ts b/src/frontend/src/types/mcp/index.ts
index 329a88148e12..aed741c45dfb 100644
--- a/src/frontend/src/types/mcp/index.ts
+++ b/src/frontend/src/types/mcp/index.ts
@@ -1,3 +1,22 @@
+export type AuthSettingsType = {
+ auth_type: string;
+ api_key?: string;
+ username?: string;
+ password?: string;
+ bearer_token?: string;
+ iam_endpoint?: string;
+ oauth_host?: string;
+ oauth_port?: string;
+ oauth_server_url?: string;
+ oauth_callback_path?: string;
+ oauth_client_id?: string;
+ oauth_client_secret?: string;
+ oauth_auth_url?: string;
+ oauth_token_url?: string;
+ oauth_mcp_scope?: string;
+ oauth_provider_scope?: string;
+};
+
export type MCPSettingsType = {
id: string;
mcp_enabled: boolean;
@@ -8,6 +27,11 @@ export type MCPSettingsType = {
input_schema?: Record
;
};
+export type MCPProjectResponseType = {
+ tools: MCPSettingsType[];
+ auth_settings?: AuthSettingsType;
+};
+
export type MCPServerInfoType = {
id?: string;
name: string;
diff --git a/src/frontend/src/utils/mcpUtils.ts b/src/frontend/src/utils/mcpUtils.ts
index e1b2667b1346..f1a109493228 100644
--- a/src/frontend/src/utils/mcpUtils.ts
+++ b/src/frontend/src/utils/mcpUtils.ts
@@ -1,5 +1,28 @@
import { MCPServerType } from "@/types/mcp";
+export enum AuthMethodId {
+ NONE = "none",
+ API_KEY = "apikey",
+ BASIC = "basic",
+ BEARER = "bearer",
+ IAM = "iam",
+ OAUTH = "oauth",
+}
+
+export const AUTH_METHODS = {
+ [AuthMethodId.NONE]: { id: AuthMethodId.NONE, label: "None" },
+ [AuthMethodId.API_KEY]: { id: AuthMethodId.API_KEY, label: "API Key" },
+ [AuthMethodId.BASIC]: {
+ id: AuthMethodId.BASIC,
+ label: "Basic",
+ },
+ [AuthMethodId.BEARER]: { id: AuthMethodId.BEARER, label: "Bearer Token" },
+ [AuthMethodId.IAM]: { id: AuthMethodId.IAM, label: "IAM" },
+ [AuthMethodId.OAUTH]: { id: AuthMethodId.OAUTH, label: "OAuth" },
+} as const;
+
+export const AUTH_METHODS_ARRAY = Object.values(AUTH_METHODS);
+
/**
* Extracts all MCP servers from a JSON string or object.
* Supports:
diff --git a/src/frontend/vite.config.mts b/src/frontend/vite.config.mts
index 4b7bee6355b9..662a3270bec9 100644
--- a/src/frontend/vite.config.mts
+++ b/src/frontend/vite.config.mts
@@ -53,6 +53,9 @@ export default defineConfig(({ mode }) => {
"process.env.LANGFLOW_AUTO_LOGIN": JSON.stringify(
envLangflow.LANGFLOW_AUTO_LOGIN ?? true,
),
+ "process.env.LANGFLOW_FEATURE_MCP_COMPOSER": JSON.stringify(
+ envLangflow.LANGFLOW_FEATURE_MCP_COMPOSER ?? "false",
+ ),
},
plugins: [react(), svgr(), tsconfigPaths()],
server: {