Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
80a9512
init auth forms
mfortman11 Jul 17, 2025
5cd24b3
Add iam endpoint
lucaseduoli Jul 17, 2025
1e20c9e
Add radix radio
lucaseduoli Jul 17, 2025
a7d7ea7
Add radio group UI element
lucaseduoli Jul 17, 2025
3594fe3
Add IAM endpoint to types
lucaseduoli Jul 17, 2025
9b3723c
Add auth modal
lucaseduoli Jul 17, 2025
a7b0bdc
Remove auth from tools component
lucaseduoli Jul 17, 2025
4a763b3
Add auth modal to mcp server tab
lucaseduoli Jul 17, 2025
5013477
Add placeholders to fields
lucaseduoli Jul 17, 2025
3534a9b
Add dynamic headers
lucaseduoli Jul 17, 2025
dbcda3e
changed authentication name
lucaseduoli Jul 17, 2025
d49132b
CHanged paddings
lucaseduoli Jul 17, 2025
1d2852e
Added header and button under feature flag
lucaseduoli Jul 17, 2025
bcb9a98
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2025
3b76b63
update api key form field
mfortman11 Jul 17, 2025
c512d3a
add credential handling and fix feature flag
mfortman11 Jul 17, 2025
a8e0018
Merge branch 'main' of github.com:langflow-ai/langflow into mcp-auth-…
mfortman11 Jul 17, 2025
b842775
Update autologin condition
mfortman11 Jul 17, 2025
b3e3648
design update
mfortman11 Jul 17, 2025
fd8471f
style updates
mfortman11 Jul 18, 2025
023ff9e
Merge branch 'main' of github.com:langflow-ai/langflow into mcp-auth-…
mfortman11 Jul 18, 2025
8811ef3
Merge branch 'main' into mcp-auth-forms
mfortman11 Jul 21, 2025
683547c
Merge branch 'main' into mcp-auth-forms
mfortman11 Jul 21, 2025
efd0462
ci details
mfortman11 Jul 21, 2025
baccb5f
Merge branch 'mcp-auth-forms' of github.com:langflow-ai/langflow into…
mfortman11 Jul 21, 2025
91e18e8
Merge branch 'main' into mcp-auth-forms
mfortman11 Jul 21, 2025
ca59a9c
revert ci logs
mfortman11 Jul 21, 2025
fadf684
test update, ff name update, and username + pass -> basic
mfortman11 Jul 21, 2025
c561086
restore ci
mfortman11 Jul 21, 2025
7487473
default fix
mfortman11 Jul 21, 2025
82ff903
added iam endpoint
lucaseduoli Jul 22, 2025
4b8738d
add oauth
mfortman11 Jul 22, 2025
cb5bcf8
Merge branch 'mcp-auth-forms' of github.com:langflow-ai/langflow into…
mfortman11 Jul 22, 2025
0a75b6c
remove secretstr
mfortman11 Jul 22, 2025
9a847b4
updated backend test and schema
lucaseduoli Jul 22, 2025
98056ff
updated test
lucaseduoli Jul 22, 2025
51e8eff
updated test user can update
lucaseduoli Jul 22, 2025
8e7b162
test fix
mfortman11 Jul 22, 2025
03d4455
Merge branch 'mcp-auth-forms' of github.com:langflow-ai/langflow into…
mfortman11 Jul 22, 2025
5eeb83c
Merge branch 'main' into mcp-auth-forms
mfortman11 Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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")
39 changes: 30 additions & 9 deletions src/backend/base/langflow/api/v1/mcp_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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}"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions src/backend/base/langflow/api/v1/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BaseModel,
ConfigDict,
Field,
SecretStr,
field_serializer,
field_validator,
model_serializer,
Expand Down Expand Up @@ -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."""

Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

class FeatureFlags(BaseSettings):
mvp_components: bool = False
mcp_composer: bool = True

class Config:
env_prefix = "LANGFLOW_FEATURE_"
Expand Down
78 changes: 54 additions & 24 deletions src/backend/tests/unit/api/v1/test_mcp_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading