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_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

View file

@ -8,6 +8,7 @@ from pydantic import (
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

View file

@ -2,7 +2,7 @@ from typing import Optional
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 @@ from langflow.services.database.models.user.model import User
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

View file

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