feat: adds authentication to mcp server under feature flag (#9095)

* init auth forms

* Add iam endpoint

* Add radix radio

* Add radio group UI element

* Add IAM endpoint to types

* Add auth modal

* Remove auth from tools component

* Add auth modal to mcp server tab

* Add placeholders to fields

* Add dynamic headers

* changed authentication name

* CHanged paddings

* Added header and button under feature flag

* [autofix.ci] apply automated fixes

* update api key form field

* add credential handling and fix feature flag

* Update autologin condition

* design update

* style updates

* ci details

* revert ci logs

* test update, ff name update, and username + pass -> basic

* restore ci

* default fix

* added iam endpoint

* add oauth

* remove secretstr

* updated backend test and schema

* updated test

* updated test user can update

* test fix

---------

Co-authored-by: Mike Fortman <michael.fortman@datastax.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Lucas Oliveira 2025-07-22 17:04:47 -03:00 • committed by GitHub
commit 9e24202466
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1066 additions and 141 deletions

View file

@ -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")

View file

@ -29,11 +29,17 @@ from langflow.api.v1.mcp_utils import (
handle_mcp_errors, handle_mcp_errors,
handle_read_resource, 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.constants import MAX_MCP_SERVER_NAME_LENGTH
from langflow.base.mcp.util import sanitize_mcp_name from langflow.base.mcp.util import sanitize_mcp_name
from langflow.services.database.models import Flow, Folder from langflow.services.database.models import Flow, Folder
from langflow.services.deps import get_settings_service, session_scope from langflow.services.deps import get_settings_service, session_scope
from langflow.services.settings.feature_flags import FEATURE_FLAGS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -60,7 +66,7 @@ async def list_project_tools(
current_user: CurrentActiveMCPUser, current_user: CurrentActiveMCPUser,
*, *,
mcp_enabled: bool = True, mcp_enabled: bool = True,
) -> list[MCPSettings]: ) -> MCPProjectResponse:
"""List all tools in a project that are enabled for MCP.""" """List all tools in a project that are enabled for MCP."""
tools: list[MCPSettings] = [] tools: list[MCPSettings] = []
try: try:
@ -114,12 +120,19 @@ async def list_project_tools(
logger.warning(msg) logger.warning(msg)
continue 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: except Exception as e:
msg = f"Error listing project tools: {e!s}" msg = f"Error listing project tools: {e!s}"
logger.exception(msg) logger.exception(msg)
raise HTTPException(status_code=500, detail=str(e)) from e 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) @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) @router.patch("/{project_id}", status_code=200)
async def update_project_mcp_settings( async def update_project_mcp_settings(
project_id: UUID, project_id: UUID,
settings: list[MCPSettings], request: MCPProjectUpdateRequest,
current_user: CurrentActiveMCPUser, 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: try:
async with session_scope() as session: async with session_scope() as session:
# Fetch the project first to verify it exists and belongs to the current user # 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: if not project:
raise HTTPException(status_code=404, detail="Project not found") 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 # Query flows in the project
flows = (await session.exec(select(Flow).where(Flow.folder_id == project_id))).all() 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 = [] updated_flows = []
for flow in flows: for flow in flows:
@ -256,7 +276,7 @@ async def update_project_mcp_settings(
await session.commit() 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: except Exception as e:
msg = f"Error updating project MCP settings: {e!s}" 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 # Determine command and args based on operating system
os_type = platform.system() os_type = platform.system()
command = "uvx" 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) # 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() 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": if os_type == "Windows":
command = "cmd" command = "cmd"
args = ["/c", "uvx", "mcp-proxy", sse_url] args = ["/c", "uvx", mcp_tool, sse_url]
logger.debug("Windows detected, using cmd command") logger.debug("Windows detected, using cmd command")
name = project.name name = project.name

View file

@ -8,6 +8,7 @@ from pydantic import (
BaseModel, BaseModel,
ConfigDict, ConfigDict,
Field, Field,
SecretStr,
field_serializer, field_serializer,
field_validator, field_validator,
model_serializer, model_serializer,
@ -440,6 +441,27 @@ class CancelFlowResponse(BaseModel):
message: str 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): class MCPSettings(BaseModel):
"""Model representing MCP settings for a flow.""" """Model representing MCP settings for a flow."""
@ -451,5 +473,19 @@ class MCPSettings(BaseModel):
description: str | None = None 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): class MCPInstallRequest(BaseModel):
client: str client: str

View file

@ -2,7 +2,7 @@ from typing import Optional
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from sqlalchemy import Text, UniqueConstraint 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.flow.model import Flow, FlowRead
from langflow.services.database.models.user.model import User from langflow.services.database.models.user.model import User
@ -11,6 +11,11 @@ from langflow.services.database.models.user.model import User
class FolderBase(SQLModel): class FolderBase(SQLModel):
name: str = Field(index=True) name: str = Field(index=True)
description: str | None = Field(default=None, sa_column=Column(Text)) 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] class Folder(FolderBase, table=True): # type: ignore[call-arg]
@ -53,3 +58,4 @@ class FolderUpdate(SQLModel):
parent_id: UUID | None = None parent_id: UUID | None = None
components: list[UUID] = Field(default_factory=list) components: list[UUID] = Field(default_factory=list)
flows: list[UUID] = Field(default_factory=list) flows: list[UUID] = Field(default_factory=list)
auth_settings: dict | None = None

View file

@ -3,6 +3,7 @@ from pydantic_settings import BaseSettings
class FeatureFlags(BaseSettings): class FeatureFlags(BaseSettings):
mvp_components: bool = False mvp_components: bool = False
mcp_composer: bool = True
class Config: class Config:
env_prefix = "LANGFLOW_FEATURE_" env_prefix = "LANGFLOW_FEATURE_"

View file

@ -206,20 +206,30 @@ async def test_update_project_mcp_settings_success(
): ):
"""Test successful update of MCP settings using real database.""" """Test successful update of MCP settings using real database."""
# Create settings for updating the flow # Create settings for updating the flow
settings = [ json_payload = {
{ "settings": [
"id": str(test_flow_for_update.id), {
"action_name": "updated_action", "id": str(test_flow_for_update.id),
"action_description": "Updated description", "action_name": "updated_action",
"mcp_enabled": False, "action_description": "Updated description",
"name": test_flow_for_update.name, "mcp_enabled": False,
"description": test_flow_for_update.description, "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 # Make the real PATCH request
response = await client.patch( 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 # 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 # Use real database objects instead of mocks to avoid the coroutine issue
# Empty settings list # 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 # Make the request to the actual endpoint
response = await client.patch( 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 # 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.""" """Test that a user can update MCP settings for their own flows using real database."""
# User attempts to update their own flow settings # User attempts to update their own flow settings
updated_settings = [ json_payload = {
{ "settings": [
"id": str(user_test_flow.id), {
"action_name": "updated_user_action", "id": str(user_test_flow.id),
"action_description": "Updated user action description", "action_name": "updated_user_action",
"mcp_enabled": False, "action_description": "Updated user action description",
"name": "User Test Flow", "mcp_enabled": False,
"description": "This flow belongs to the active user", "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 # Make the PATCH request to update settings
response = await client.patch( 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 # Should succeed as the user owns this project and flow

View file

@ -20,6 +20,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@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-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.2.1", "@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": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10", "version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",

View file

@ -15,6 +15,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@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-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slider": "^1.2.1",

View file

@ -1,5 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { ICON_STROKE_WIDTH } from "@/constants/constants"; import { ICON_STROKE_WIDTH } from "@/constants/constants";
import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags";
import ToolsModal from "@/modals/toolsModal"; import ToolsModal from "@/modals/toolsModal";
import { cn, testIdCase } from "@/utils/utils"; import { cn, testIdCase } from "@/utils/utils";
import { ForwardedIconComponent } from "../../../../common/genericIconComponent"; import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
@ -59,28 +61,47 @@ export default function ToolsComponent({
icon={icon} icon={icon}
/> />
<div <div
className="relative flex w-full items-center gap-3" className="relative flex flex-col w-full gap-3"
data-testid={"div-" + id} data-testid={"div-" + id}
> >
{(visibleActions.length > 0 || isAction) && ( <div className="flex flex-row justify-between">
<Button <div className="flex flex-row justify-between">
variant={"ghost"} <ShadTooltip
disabled={!value || disabled} content="Flows in this project can be exposed as callable MCP tools."
size={"iconMd"} side="right"
className={cn( >
"absolute -top-8 right-0 !text-mmd font-normal text-muted-foreground group-hover:text-primary", <div
)} className={cn(
data-testid="button_open_actions" "flex items-center hover:cursor-help",
onClick={() => setIsModalOpen(true)} !ENABLE_MCP_COMPOSER && "text-mmd",
> )}
<ForwardedIconComponent >
name="Settings2" Flows/Tools
className="icon-size" <ForwardedIconComponent
strokeWidth={ICON_STROKE_WIDTH} name="info"
/> className="ml-1.5 h-4 w-4 text-muted-foreground"
{button_description} aria-hidden="true"
</Button> />
)} </div>
</ShadTooltip>
</div>
{(visibleActions.length > 0 || isAction) && (
<Button
variant={ENABLE_MCP_COMPOSER ? "outline" : "ghost"}
disabled={!value || disabled}
size="sm"
data-testid="button_open_actions"
onClick={() => setIsModalOpen(true)}
>
<ForwardedIconComponent
name={ENABLE_MCP_COMPOSER ? "wrench" : "Settings2"}
className="icon-size"
strokeWidth={ICON_STROKE_WIDTH}
/>
{button_description}
</Button>
)}
</div>
{!value ? ( {!value ? (
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5"> <div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
{[...Array(4)].map((_, index) => ( {[...Array(4)].map((_, index) => (

View file

@ -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<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View file

@ -1,5 +1,5 @@
import type { useQueryFunctionType } from "@/types/api"; import type { useQueryFunctionType } from "@/types/api";
import type { MCPSettingsType } from "@/types/mcp"; import type { MCPProjectResponseType } from "@/types/mcp";
import { api } from "../../api"; import { api } from "../../api";
import { getURL } from "../../helpers/constants"; import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor"; import { UseRequestProcessor } from "../../services/request-processor";
@ -8,7 +8,7 @@ interface IGetFlowsMCP {
projectId: string; projectId: string;
} }
type getFlowsMCPResponse = Array<MCPSettingsType>; type getFlowsMCPResponse = MCPProjectResponseType;
export const useGetFlowsMCP: useQueryFunctionType< export const useGetFlowsMCP: useQueryFunctionType<
IGetFlowsMCP, IGetFlowsMCP,
@ -24,7 +24,7 @@ export const useGetFlowsMCP: useQueryFunctionType<
return data; return data;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return []; return { tools: [], auth_settings: undefined };
} }
}; };

View file

@ -1,6 +1,6 @@
import type { UseMutationResult } from "@tanstack/react-query"; import type { UseMutationResult } from "@tanstack/react-query";
import type { useMutationFunctionType } from "@/types/api"; import type { useMutationFunctionType } from "@/types/api";
import type { MCPSettingsType } from "@/types/mcp"; import type { AuthSettingsType, MCPSettingsType } from "@/types/mcp";
import { api } from "../../api"; import { api } from "../../api";
import { getURL } from "../../helpers/constants"; import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor"; import { UseRequestProcessor } from "../../services/request-processor";
@ -9,21 +9,26 @@ interface PatchFlowMCPParams {
project_id: string; project_id: string;
} }
interface PatchFlowMCPRequest {
settings: MCPSettingsType[];
auth_settings?: AuthSettingsType;
}
interface PatchFlowMCPResponse { interface PatchFlowMCPResponse {
message: string; message: string;
} }
export const usePatchFlowsMCP: useMutationFunctionType< export const usePatchFlowsMCP: useMutationFunctionType<
PatchFlowMCPParams, PatchFlowMCPParams,
MCPSettingsType[], PatchFlowMCPRequest,
PatchFlowMCPResponse PatchFlowMCPResponse
> = (params, options?) => { > = (params, options?) => {
const { mutate, queryClient } = UseRequestProcessor(); const { mutate, queryClient } = UseRequestProcessor();
async function patchFlowMCP(flowMCP: MCPSettingsType[]): Promise<any> { async function patchFlowMCP(requestData: PatchFlowMCPRequest): Promise<any> {
const res = await api.patch( const res = await api.patch(
`${getURL("MCP")}/${params.project_id}`, `${getURL("MCP")}/${params.project_id}`,
flowMCP, requestData,
); );
return res.data.message; return res.data.message;
} }
@ -31,7 +36,7 @@ export const usePatchFlowsMCP: useMutationFunctionType<
const mutation: UseMutationResult< const mutation: UseMutationResult<
PatchFlowMCPResponse, PatchFlowMCPResponse,
any, any,
MCPSettingsType[] PatchFlowMCPRequest
> = mutate(["usePatchFlowsMCP"], patchFlowMCP, { > = mutate(["usePatchFlowsMCP"], patchFlowMCP, {
onSettled: () => { onSettled: () => {
queryClient.refetchQueries({ queryKey: ["useGetFlowsMCP"] }); queryClient.refetchQueries({ queryKey: ["useGetFlowsMCP"] });

View file

@ -15,3 +15,5 @@ export const ENABLE_VOICE_ASSISTANT = true;
export const ENABLE_IMAGE_ON_PLAYGROUND = false; export const ENABLE_IMAGE_ON_PLAYGROUND = false;
export const ENABLE_MCP = true; export const ENABLE_MCP = true;
export const ENABLE_MCP_NOTICE = false; export const ENABLE_MCP_NOTICE = false;
export const ENABLE_MCP_COMPOSER =
process.env.LANGFLOW_FEATURE_MCP_COMPOSER === "true";

View file

@ -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<string>(
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 (
<BaseModal open={open} setOpen={setOpen} size="small" className="p-4">
<BaseModal.Header>
<div className="flex items-center gap-2 text-base">Authentication</div>
</BaseModal.Header>
<BaseModal.Content className="h-full" overflowHidden>
<div className="flex gap-6 border rounded-md p-4 h-full min-h-[180px]">
{/* Left column - Radio buttons */}
<div className="flex flex-col flex-1 items-start">
<RadioGroup value={authType} onValueChange={handleAuthTypeChange}>
{AUTH_METHODS_ARRAY.map((option) => (
<div key={option.id} className="flex items-center space-x-2">
<RadioGroupItem value={option.id} id={option.id} />
<Label
htmlFor={option.id}
className="!text-mmd font-normal cursor-pointer flex items-center gap-3"
>
{option.label}
{option.id === "none" && authType === "none" && (
<span className="text-accent-amber-foreground flex gap-1.5 text-xs items-center">
<ForwardedIconComponent
name="AlertTriangle"
className="h-3.5 w-3.5 shrink-0"
/>
Public endpoint - no auth. Use only in dev or trusted
envs.
</span>
)}
</Label>
</div>
))}
</RadioGroup>
</div>
{authType !== "none" && <Separator orientation="vertical" />}
{/* Right column - Input fields */}
{authType !== "none" && (
<div className="w-3/5 min-h-[136px]">
{authType === "apikey" && (
<div className="flex flex-col items-start gap-2">
<Label htmlFor="api-key" className="!text-mmd font-medium">
API Key Value
</Label>
<Input
id="api-key"
type="password"
placeholder="Enter API Key"
value={authFields.apiKey || ""}
onChange={(e) =>
handleAuthFieldChange("apiKey", e.target.value)
}
/>
</div>
)}
{authType === "basic" && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="username" className="!text-mmd font-medium">
Username
</Label>
<Input
id="username"
type="text"
placeholder="Enter Username"
value={authFields.username || ""}
onChange={(e) =>
handleAuthFieldChange("username", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="password" className="!text-mmd font-medium">
Password
</Label>
<Input
id="password"
type="password"
placeholder="Enter Password"
value={authFields.password || ""}
onChange={(e) =>
handleAuthFieldChange("password", e.target.value)
}
/>
</div>
</div>
)}
{authType === "bearer" && (
<div className="flex flex-col gap-2">
<Label
htmlFor="bearer-token"
className="!text-mmd font-medium"
>
Bearer Token
</Label>
<Input
id="bearer-token"
type="password"
placeholder="Enter Bearer Token"
value={authFields.bearerToken || ""}
onChange={(e) =>
handleAuthFieldChange("bearerToken", e.target.value)
}
/>
</div>
)}
{authType === "iam" && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label
htmlFor="iam-endpoint"
className="!text-mmd font-medium"
>
IAM Endpoint
</Label>
<Input
id="iam-endpoint"
type="text"
placeholder="Enter IAM Endpoint"
value={authFields.iamEndpoint || ""}
onChange={(e) =>
handleAuthFieldChange("iamEndpoint", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="api-key" className="!text-mmd font-medium">
API Key or Token
</Label>
<Input
id="password"
type="password"
placeholder="Enter API Key or Token"
value={authFields.apiKey || ""}
onChange={(e) =>
handleAuthFieldChange("apiKey", e.target.value)
}
/>
</div>
</div>
)}
{authType === "oauth" && (
<div className="flex flex-col gap-3 h-full overflow-y-auto">
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-host"
className="!text-mmd font-medium"
>
OAuth Host
</Label>
<Input
id="oauth-host"
type="text"
placeholder="localhost"
value={authFields.oauthHost || ""}
onChange={(e) =>
handleAuthFieldChange("oauthHost", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-port"
className="!text-mmd font-medium"
>
OAuth Port
</Label>
<Input
id="oauth-port"
type="text"
placeholder="1234"
value={authFields.oauthPort || ""}
onChange={(e) =>
handleAuthFieldChange("oauthPort", e.target.value)
}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-server-url"
className="!text-mmd font-medium"
>
OAuth Server URL
</Label>
<Input
id="oauth-server-url"
type="text"
placeholder="http://localhost:1234"
value={authFields.oauthServerUrl || ""}
onChange={(e) =>
handleAuthFieldChange("oauthServerUrl", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-callback-path"
className="!text-mmd font-medium"
>
OAuth Callback Path
</Label>
<Input
id="oauth-callback-path"
type="text"
placeholder="http://localhost:9000/auth/idaas/callback"
value={authFields.oauthCallbackPath || ""}
onChange={(e) =>
handleAuthFieldChange(
"oauthCallbackPath",
e.target.value,
)
}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-client-id"
className="!text-mmd font-medium"
>
Client ID
</Label>
<Input
id="oauth-client-id"
type="text"
placeholder="Enter Client ID"
value={authFields.oauthClientId || ""}
onChange={(e) =>
handleAuthFieldChange("oauthClientId", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-client-secret"
className="!text-mmd font-medium"
>
Client Secret
</Label>
<Input
id="oauth-client-secret"
type="password"
placeholder="Enter Client Secret"
value={authFields.oauthClientSecret || ""}
onChange={(e) =>
handleAuthFieldChange(
"oauthClientSecret",
e.target.value,
)
}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-auth-url"
className="!text-mmd font-medium"
>
Authorization URL
</Label>
<Input
id="oauth-auth-url"
type="text"
value={authFields.oauthAuthUrl || ""}
onChange={(e) =>
handleAuthFieldChange("oauthAuthUrl", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-token-url"
className="!text-mmd font-medium"
>
Token URL
</Label>
<Input
id="oauth-token-url"
type="text"
value={authFields.oauthTokenUrl || ""}
onChange={(e) =>
handleAuthFieldChange("oauthTokenUrl", e.target.value)
}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-mcp-scope"
className="!text-mmd font-medium"
>
MCP Scope
</Label>
<Input
id="oauth-mcp-scope"
type="text"
placeholder="user"
value={authFields.oauthMcpScope || ""}
onChange={(e) =>
handleAuthFieldChange("oauthMcpScope", e.target.value)
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="oauth-provider-scope"
className="!text-mmd font-medium"
>
Provider Scope
</Label>
<Input
id="oauth-provider-scope"
type="text"
placeholder="openid"
value={authFields.oauthProviderScope || ""}
onChange={(e) =>
handleAuthFieldChange(
"oauthProviderScope",
e.target.value,
)
}
/>
</div>
</div>
</div>
)}
</div>
)}
</div>
</BaseModal.Content>
<BaseModal.Footer
submit={{
label: "Save Authentication",
onClick: handleSave,
}}
/>
</BaseModal>
);
};
export default AuthModal;

View file

@ -73,18 +73,20 @@ const ToolsModal = forwardRef<AgGridReact, ToolsModalProps>(
</div> </div>
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content overflowHidden className="flex flex-col p-0"> <BaseModal.Content overflowHidden className="flex flex-col p-0">
<div className="flex h-full"> <div className="flex flex-col w-full h-full">
<SidebarProvider width="20rem" defaultOpen={false}> <div className="flex h-full">
<ToolsTable <SidebarProvider width="20rem" defaultOpen={false}>
rows={rows} <ToolsTable
isAction={isAction} rows={rows}
placeholder={placeholder} isAction={isAction}
data={data} placeholder={placeholder}
setData={setData} data={data}
open={open} setData={setData}
handleOnNewValue={handleOnNewValue} open={open}
/> handleOnNewValue={handleOnNewValue}
</SidebarProvider> />
</SidebarProvider>
</div>
</div> </div>
</BaseModal.Content> </BaseModal.Content>
</BaseModal> </BaseModal>

View file

@ -14,13 +14,16 @@ import {
} from "@/controllers/API/queries/mcp"; } from "@/controllers/API/queries/mcp";
import { useGetInstalledMCP } from "@/controllers/API/queries/mcp/use-get-installed-mcp"; import { useGetInstalledMCP } from "@/controllers/API/queries/mcp/use-get-installed-mcp";
import { usePatchInstallMCP } from "@/controllers/API/queries/mcp/use-patch-install-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 { useCustomIsLocalConnection } from "@/customization/hooks/use-custom-is-local-connection";
import useTheme from "@/customization/hooks/use-custom-theme"; import useTheme from "@/customization/hooks/use-custom-theme";
import { customGetMCPUrl } from "@/customization/utils/custom-mcp-url"; import { customGetMCPUrl } from "@/customization/utils/custom-mcp-url";
import AuthModal from "@/modals/authModal";
import useAlertStore from "@/stores/alertStore"; import useAlertStore from "@/stores/alertStore";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import { useFolderStore } from "@/stores/foldersStore"; 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 { parseString } from "@/utils/stringManipulation";
import { cn, getOS } from "@/utils/utils"; import { cn, getOS } from "@/utils/utils";
@ -131,11 +134,16 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const [apiKey, setApiKey] = useState<string>(""); const [apiKey, setApiKey] = useState<string>("");
const [isGeneratingApiKey, setIsGeneratingApiKey] = useState(false); const [isGeneratingApiKey, setIsGeneratingApiKey] = useState(false);
const [authModalOpen, setAuthModalOpen] = useState(false);
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const { data: flowsMCP } = useGetFlowsMCP({ projectId }); const { data: mcpProjectData } = useGetFlowsMCP({ projectId });
const { mutate: patchFlowsMCP } = usePatchFlowsMCP({ project_id: 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({ const { mutate: patchInstallMCP } = usePatchInstallMCP({
project_id: projectId, project_id: projectId,
}); });
@ -156,14 +164,44 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
isLocalConnection ? "Auto install" : "JSON", isLocalConnection ? "Auto install" : "JSON",
); );
const handleOnNewValue = (value) => { const handleOnNewValue = (value: any) => {
const flowsMCPData: MCPSettingsType[] = value.value.map((flow) => ({ const flowsMCPData: MCPSettingsType[] = value.value.map((flow: any) => ({
id: flow.id, id: flow.id,
action_name: flow.name, action_name: flow.name,
action_description: flow.description, action_description: flow.description,
mcp_enabled: flow.status, 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) => ({ const flowsMCPData = flowsMCP?.map((flow) => ({
@ -187,10 +225,93 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const apiUrl = customGetMCPUrl(projectId); 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 = `{ const MCP_SERVER_JSON = `{
"mcpServers": { "mcpServers": {
"lf-${parseString(folderName ?? "project", ["snake_case", "no_blank", "lowercase"]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": { "lf-${parseString(folderName ?? "project", [
"command": "${selectedPlatform === "windows" ? "cmd" : selectedPlatform === "wsl" ? "wsl" : "uvx"}", "snake_case",
"no_blank",
"lowercase",
]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
"command": "${
selectedPlatform === "windows"
? "cmd"
: selectedPlatform === "wsl"
? "wsl"
: "uvx"
}",
"args": [ "args": [
${ ${
selectedPlatform === `windows` selectedPlatform === `windows`
@ -201,16 +322,9 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
? `"uvx", ? `"uvx",
` `
: "" : ""
}"mcp-proxy",${ }"${ENABLE_MCP_COMPOSER ? "mcp-composer" : "mcp-proxy"}",${getAuthHeaders()}
isAutoLogin
? ""
: `
"--headers",
"x-api-key",
"${apiKey || "YOUR_API_KEY"}",`
}
"${apiUrl}" "${apiUrl}"
] ]${ENABLE_MCP_COMPOSER && currentAuthSettings?.auth_type === "oauth" ? `,` : ""}${getEnvVars()}
} }
} }
}`; }`;
@ -239,7 +353,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
.then((res) => { .then((res) => {
setApiKey(res["api_key"]); setApiKey(res["api_key"]);
}) })
.catch((err) => {}) .catch(() => {})
.finally(() => { .finally(() => {
setIsGeneratingApiKey(false); setIsGeneratingApiKey(false);
}); });
@ -247,42 +361,35 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const [loadingMCP, setLoadingMCP] = useState<string[]>([]); const [loadingMCP, setLoadingMCP] = useState<string[]>([]);
// Check if authentication is configured (not "none")
const hasAuthentication =
currentAuthSettings?.auth_type && currentAuthSettings.auth_type !== "none";
return ( return (
<div> <div>
<div className="pb-2 text-sm font-medium" data-testid="mcp-server-title"> <div className="flex justify-between gap-4 items-start">
MCP Server <div>
</div> <div className="pb-2 font-medium" data-testid="mcp-server-title">
<div className="pb-4 text-mmd text-muted-foreground"> MCP Server
Access your Project's flows as Tools within a MCP Server. Learn more in </div>
our <div className="pb-4 text-mmd text-muted-foreground">
<a Access your Project's flows as Tools within a MCP Server. Learn more
className="text-accent-pink-foreground" in our
href={MCP_SERVER_DEPLOY_TUTORIAL_LINK} <a
target="_blank" className="text-accent-pink-foreground"
rel="noreferrer" href={MCP_SERVER_DEPLOY_TUTORIAL_LINK}
> target="_blank"
{" "} rel="noreferrer"
Projects as MCP Servers guide. >
</a> {" "}
Projects as MCP Servers guide.
</a>
</div>
</div>
</div> </div>
<div className="flex flex-col justify-between gap-8 xl:flex-row"> <div className="flex flex-col justify-between gap-8 xl:flex-row">
<div className="w-full xl:w-2/5"> <div className="w-full xl:w-2/5">
<div className="flex flex-row justify-between"> <div className="flex flex-row flex-wrap gap-2">
<ShadTooltip
content="Flows in this project can be exposed as callable MCP tools."
side="right"
>
<div className="flex items-center text-mmd font-medium hover:cursor-help">
Flows/Tools
<ForwardedIconComponent
name="info"
className="ml-1.5 h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
</div>
</ShadTooltip>
</div>
<div className="flex flex-row flex-wrap gap-2 pt-2">
<ToolsComponent <ToolsComponent
value={flowsMCPData} value={flowsMCPData}
title="MCP Server Tools" title="MCP Server Tools"
@ -297,24 +404,59 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
</div> </div>
</div> </div>
<div className="flex flex-1 flex-col gap-4 overflow-hidden"> <div className="flex flex-1 flex-col gap-4 overflow-hidden">
<div className="flex flex-col"> {ENABLE_MCP_COMPOSER && (
<div className="flex justify-between">
<span className="flex gap-2 items-center">
Auth:
{!hasAuthentication ? (
<span className="text-accent-amber-foreground flex gap-2 text-mmd items-center">
<ForwardedIconComponent
name="AlertTriangle"
className="h-4 w-4 shrink-0"
/>
None (public)
</span>
) : (
<span className="text-accent-emerald-foreground flex gap-2 text-mmd items-center">
<ForwardedIconComponent
name="Check"
className="h-4 w-4 shrink-0"
/>
{AUTH_METHODS[
currentAuthSettings.auth_type as keyof typeof AUTH_METHODS
]?.label || currentAuthSettings.auth_type}
</span>
)}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setAuthModalOpen(true)}
>
<ForwardedIconComponent
name="Fingerprint"
className="h-4 w-4 shrink-0"
/>
{hasAuthentication ? "Edit Auth" : "Add Auth"}
</Button>
</div>
)}
<div className={cn("flex flex-col", !ENABLE_MCP_COMPOSER && "mt-2")}>
<div className="flex flex-row justify-start border-b border-border"> <div className="flex flex-row justify-start border-b border-border">
{[{ name: "Auto install" }, { name: "JSON" }].map( {[{ name: "Auto install" }, { name: "JSON" }].map((item) => (
(item, index) => ( <Button
<Button unstyled
unstyled key={item.name}
key={item.name} className={`flex h-6 flex-row items-end gap-2 text-nowrap border-b-2 border-border border-b-transparent !py-1 font-medium ${
className={`flex h-6 flex-row items-end gap-2 text-nowrap border-b-2 border-border border-b-transparent !py-1 font-medium ${ selectedMode === item.name
selectedMode === item.name ? "border-b-2 border-black dark:border-b-white"
? "border-b-2 border-black dark:border-b-white" : "text-muted-foreground hover:text-foreground"
: "text-muted-foreground hover:text-foreground" } px-3 py-2 text-[13px]`}
} px-3 py-2 text-[13px]`} onClick={() => setSelectedMode(item.name)}
onClick={() => setSelectedMode(item.name)} >
> <span>{item.name}</span>
<span>{item.name}</span> </Button>
</Button> ))}
),
)}
</div> </div>
</div> </div>
{selectedMode === "JSON" && ( {selectedMode === "JSON" && (
@ -325,10 +467,10 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
onValueChange={setSelectedPlatform} onValueChange={setSelectedPlatform}
> >
<TabsList> <TabsList>
{operatingSystemTabs.map((tab, index) => ( {operatingSystemTabs.map((tab) => (
<TabsTrigger <TabsTrigger
className="flex items-center gap-2" className="flex items-center gap-2"
key={index} key={tab.name}
value={tab.name} value={tab.name}
> >
<ForwardedIconComponent <ForwardedIconComponent
@ -394,6 +536,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
)} )}
{autoInstallers.map((installer) => ( {autoInstallers.map((installer) => (
<Button <Button
key={installer.name}
variant="ghost" variant="ghost"
className="flex items-center justify-between disabled:text-foreground disabled:opacity-50" className="flex items-center justify-between disabled:text-foreground disabled:opacity-50"
disabled={ disabled={
@ -461,6 +604,14 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
)} )}
</div> </div>
</div> </div>
{ENABLE_MCP_COMPOSER && (
<AuthModal
open={authModalOpen}
setOpen={setAuthModalOpen}
authSettings={currentAuthSettings}
onSave={handleAuthSave}
/>
)}
</div> </div>
); );
}; };

View file

@ -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 = { export type MCPSettingsType = {
id: string; id: string;
mcp_enabled: boolean; mcp_enabled: boolean;
@ -8,6 +27,11 @@ export type MCPSettingsType = {
input_schema?: Record<string, any>; input_schema?: Record<string, any>;
}; };
export type MCPProjectResponseType = {
tools: MCPSettingsType[];
auth_settings?: AuthSettingsType;
};
export type MCPServerInfoType = { export type MCPServerInfoType = {
id?: string; id?: string;
name: string; name: string;

View file

@ -1,5 +1,28 @@
import { MCPServerType } from "@/types/mcp"; 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. * Extracts all MCP servers from a JSON string or object.
* Supports: * Supports:

View file

@ -53,6 +53,9 @@ export default defineConfig(({ mode }) => {
"process.env.LANGFLOW_AUTO_LOGIN": JSON.stringify( "process.env.LANGFLOW_AUTO_LOGIN": JSON.stringify(
envLangflow.LANGFLOW_AUTO_LOGIN ?? true, envLangflow.LANGFLOW_AUTO_LOGIN ?? true,
), ),
"process.env.LANGFLOW_FEATURE_MCP_COMPOSER": JSON.stringify(
envLangflow.LANGFLOW_FEATURE_MCP_COMPOSER ?? "false",
),
}, },
plugins: [react(), svgr(), tsconfigPaths()], plugins: [react(), svgr(), tsconfigPaths()],
server: { server: {