Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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")
34 changes: 27 additions & 7 deletions src/backend/base/langflow/api/v1/mcp_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@
handle_mcp_errors,
with_db_session,
)
from langflow.api.v1.schemas import MCPInstallRequest, MCPSettings, SimplifiedAPIRequest
from langflow.api.v1.schemas import (
MCPInstallRequest,
MCPProjectResponse,
MCPProjectUpdateRequest,
MCPSettings,
SimplifiedAPIRequest,
)
from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH, MAX_MCP_TOOL_NAME_LENGTH
from langflow.base.mcp.util import get_flow_snake_case, get_unique_name, sanitize_mcp_name
from langflow.helpers.flow import json_schema_from_flow
Expand Down Expand Up @@ -64,7 +70,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 @@ -118,12 +124,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 @@ -222,10 +235,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 @@ -240,9 +253,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()
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 @@ -260,7 +280,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
26 changes: 26 additions & 0 deletions src/backend/base/langflow/api/v1/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,17 @@ class CancelFlowResponse(BaseModel):
message: str


class AuthSettings(BaseModel):
"""Model representing authentication settings for MCP."""

auth_type: str = "none"
api_key: str | None = None
username: str | None = None
password: str | None = None
bearer_token: str | None = None
iam_endpoint: str | None = None

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Add validation for auth_type field and secure password handling.

The AuthSettings model needs improvements:

  1. Add validation to ensure auth_type only accepts valid values
  2. Consider using SecretStr for sensitive fields
+from pydantic import SecretStr
+from typing import Literal

 class AuthSettings(BaseModel):
     """Model representing authentication settings for MCP."""
 
-    auth_type: str = "none"
+    auth_type: Literal["none", "apikey", "userpass", "bearer", "iam"] = "none"
     api_key: str | None = None
     username: str | None = None
-    password: str | None = None
-    bearer_token: str | None = None
+    password: SecretStr | None = None
+    bearer_token: SecretStr | None = None
     iam_endpoint: str | None = None

This ensures type safety and prevents sensitive data from being accidentally logged or exposed in error messages.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class AuthSettings(BaseModel):
"""Model representing authentication settings for MCP."""
auth_type: str = "none"
api_key: str | None = None
username: str | None = None
password: str | None = None
bearer_token: str | None = None
iam_endpoint: str | None = None
from pydantic import SecretStr
from typing import Literal
class AuthSettings(BaseModel):
"""Model representing authentication settings for MCP."""
auth_type: Literal["none", "apikey", "userpass", "bearer", "iam"] = "none"
api_key: str | None = None
username: str | None = None
password: SecretStr | None = None
bearer_token: SecretStr | None = None
iam_endpoint: str | None = None
🤖 Prompt for AI Agents
In src/backend/base/langflow/api/v1/schemas.py around lines 443 to 452, the
AuthSettings model lacks validation for the auth_type field and does not
securely handle sensitive fields. Add validation to restrict auth_type to a set
of allowed values using Pydantic's validator or Literal type. Replace the
password field type with Pydantic's SecretStr to ensure sensitive data is
protected from accidental logging or exposure.


class MCPSettings(BaseModel):
"""Model representing MCP settings for a flow."""

Expand All @@ -449,6 +460,21 @@ class MCPSettings(BaseModel):
action_description: str | None = None
name: str | None = None
description: str | None = None
auth_settings: AuthSettings | 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):
Expand Down
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
33 changes: 33 additions & 0 deletions src/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function ToolsComponent({
<div
className={cn(
"flex w-full items-center",
disabled && "cursor-not-allowed",
disabled && "cursor-not-allowed"
)}
>
<ToolsModal
Expand All @@ -68,7 +68,7 @@ export default function ToolsComponent({
disabled={!value || disabled}
size={"iconMd"}
className={cn(
"absolute -top-8 right-0 !text-mmd font-normal text-muted-foreground group-hover:text-primary",
"absolute -top-8 right-0 !text-mmd font-normal text-muted-foreground group-hover:text-primary"
)}
data-testid="button_open_actions"
onClick={() => setIsModalOpen(true)}
Expand Down
39 changes: 39 additions & 0 deletions src/frontend/src/components/ui/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-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 };
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -8,7 +8,7 @@ interface IGetFlowsMCP {
projectId: string;
}

type getFlowsMCPResponse = Array<MCPSettingsType>;
type getFlowsMCPResponse = MCPProjectResponseType;

export const useGetFlowsMCP: useQueryFunctionType<
IGetFlowsMCP,
Expand All @@ -24,7 +24,7 @@ export const useGetFlowsMCP: useQueryFunctionType<
return data;
} catch (error) {
console.error(error);
return [];
return { tools: [], auth_settings: undefined };
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,29 +9,34 @@ 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<any> {
async function patchFlowMCP(requestData: PatchFlowMCPRequest): Promise<any> {
const res = await api.patch(
`${getURL("MCP")}/${params.project_id}`,
flowMCP,
requestData,
);
return res.data.message;
}

const mutation: UseMutationResult<
PatchFlowMCPResponse,
any,
MCPSettingsType[]
PatchFlowMCPRequest
> = mutate(["usePatchFlowsMCP"], patchFlowMCP, {
onSettled: () => {
queryClient.refetchQueries({ queryKey: ["useGetFlowsMCP"] });
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/customization/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ 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_AUTH = true;
Loading
Loading