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

View file

@ -206,20 +206,30 @@ async def test_update_project_mcp_settings_success(
):
"""Test successful update of MCP settings using real database."""
# Create settings for updating the flow
settings = [
{
"id": str(test_flow_for_update.id),
"action_name": "updated_action",
"action_description": "Updated description",
"mcp_enabled": False,
"name": test_flow_for_update.name,
"description": test_flow_for_update.description,
}
]
json_payload = {
"settings": [
{
"id": str(test_flow_for_update.id),
"action_name": "updated_action",
"action_description": "Updated description",
"mcp_enabled": False,
"name": test_flow_for_update.name,
"description": test_flow_for_update.description,
}
],
"auth_settings": {
"auth_type": "none",
"api_key": None,
"iam_endpoint": None,
"username": None,
"password": None,
"bearer_token": None,
},
}
# Make the real PATCH request
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=settings
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Assert response
@ -268,11 +278,21 @@ async def test_update_project_mcp_settings_empty_settings(client: AsyncClient, u
# Use real database objects instead of mocks to avoid the coroutine issue
# Empty settings list
settings: list = []
json_payload = {
"settings": [],
"auth_settings": {
"auth_type": "none",
"api_key": None,
"iam_endpoint": None,
"username": None,
"password": None,
"bearer_token": None,
},
}
# Make the request to the actual endpoint
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=settings
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Verify response - the real endpoint should handle empty settings correctly
@ -385,20 +405,30 @@ async def test_user_can_update_own_flow_mcp_settings(
):
"""Test that a user can update MCP settings for their own flows using real database."""
# User attempts to update their own flow settings
updated_settings = [
{
"id": str(user_test_flow.id),
"action_name": "updated_user_action",
"action_description": "Updated user action description",
"mcp_enabled": False,
"name": "User Test Flow",
"description": "This flow belongs to the active user",
}
]
json_payload = {
"settings": [
{
"id": str(user_test_flow.id),
"action_name": "updated_user_action",
"action_description": "Updated user action description",
"mcp_enabled": False,
"name": "User Test Flow",
"description": "This flow belongs to the active user",
}
],
"auth_settings": {
"auth_type": "none",
"api_key": None,
"iam_endpoint": None,
"username": None,
"password": None,
"bearer_token": None,
},
}
# Make the PATCH request to update settings
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=updated_settings
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Should succeed as the user owns this project and flow

View file

@ -20,6 +20,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.2.1",
@ -5048,6 +5049,38 @@
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz",
"integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.10",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",

View file

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

View file

@ -1,5 +1,7 @@
import { useState } from "react";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { ICON_STROKE_WIDTH } from "@/constants/constants";
import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags";
import ToolsModal from "@/modals/toolsModal";
import { cn, testIdCase } from "@/utils/utils";
import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
@ -59,28 +61,47 @@ export default function ToolsComponent({
icon={icon}
/>
<div
className="relative flex w-full items-center gap-3"
className="relative flex flex-col w-full gap-3"
data-testid={"div-" + id}
>
{(visibleActions.length > 0 || isAction) && (
<Button
variant={"ghost"}
disabled={!value || disabled}
size={"iconMd"}
className={cn(
"absolute -top-8 right-0 !text-mmd font-normal text-muted-foreground group-hover:text-primary",
)}
data-testid="button_open_actions"
onClick={() => setIsModalOpen(true)}
>
<ForwardedIconComponent
name="Settings2"
className="icon-size"
strokeWidth={ICON_STROKE_WIDTH}
/>
{button_description}
</Button>
)}
<div className="flex flex-row justify-between">
<div className="flex flex-row justify-between">
<ShadTooltip
content="Flows in this project can be exposed as callable MCP tools."
side="right"
>
<div
className={cn(
"flex items-center hover:cursor-help",
!ENABLE_MCP_COMPOSER && "text-mmd",
)}
>
Flows/Tools
<ForwardedIconComponent
name="info"
className="ml-1.5 h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
</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 ? (
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
{[...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 { MCPSettingsType } from "@/types/mcp";
import type { MCPProjectResponseType } from "@/types/mcp";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
@ -8,7 +8,7 @@ interface IGetFlowsMCP {
projectId: string;
}
type getFlowsMCPResponse = Array<MCPSettingsType>;
type getFlowsMCPResponse = MCPProjectResponseType;
export const useGetFlowsMCP: useQueryFunctionType<
IGetFlowsMCP,
@ -24,7 +24,7 @@ export const useGetFlowsMCP: useQueryFunctionType<
return data;
} catch (error) {
console.error(error);
return [];
return { tools: [], auth_settings: undefined };
}
};

View file

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

View file

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

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>
</BaseModal.Header>
<BaseModal.Content overflowHidden className="flex flex-col p-0">
<div className="flex h-full">
<SidebarProvider width="20rem" defaultOpen={false}>
<ToolsTable
rows={rows}
isAction={isAction}
placeholder={placeholder}
data={data}
setData={setData}
open={open}
handleOnNewValue={handleOnNewValue}
/>
</SidebarProvider>
<div className="flex flex-col w-full h-full">
<div className="flex h-full">
<SidebarProvider width="20rem" defaultOpen={false}>
<ToolsTable
rows={rows}
isAction={isAction}
placeholder={placeholder}
data={data}
setData={setData}
open={open}
handleOnNewValue={handleOnNewValue}
/>
</SidebarProvider>
</div>
</div>
</BaseModal.Content>
</BaseModal>

View file

@ -14,13 +14,16 @@ import {
} from "@/controllers/API/queries/mcp";
import { useGetInstalledMCP } from "@/controllers/API/queries/mcp/use-get-installed-mcp";
import { usePatchInstallMCP } from "@/controllers/API/queries/mcp/use-patch-install-mcp";
import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags";
import { useCustomIsLocalConnection } from "@/customization/hooks/use-custom-is-local-connection";
import useTheme from "@/customization/hooks/use-custom-theme";
import { customGetMCPUrl } from "@/customization/utils/custom-mcp-url";
import AuthModal from "@/modals/authModal";
import useAlertStore from "@/stores/alertStore";
import useAuthStore from "@/stores/authStore";
import { useFolderStore } from "@/stores/foldersStore";
import type { MCPSettingsType } from "@/types/mcp";
import type { AuthSettingsType, MCPSettingsType } from "@/types/mcp";
import { AUTH_METHODS } from "@/utils/mcpUtils";
import { parseString } from "@/utils/stringManipulation";
import { cn, getOS } from "@/utils/utils";
@ -131,11 +134,16 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const [isCopied, setIsCopied] = useState(false);
const [apiKey, setApiKey] = useState<string>("");
const [isGeneratingApiKey, setIsGeneratingApiKey] = useState(false);
const [authModalOpen, setAuthModalOpen] = useState(false);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { data: flowsMCP } = useGetFlowsMCP({ projectId });
const { data: mcpProjectData } = useGetFlowsMCP({ projectId });
const { mutate: patchFlowsMCP } = usePatchFlowsMCP({ project_id: projectId });
// Extract tools and auth_settings from the response
const flowsMCP = mcpProjectData?.tools || [];
const currentAuthSettings = mcpProjectData?.auth_settings;
const { mutate: patchInstallMCP } = usePatchInstallMCP({
project_id: projectId,
});
@ -156,14 +164,44 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
isLocalConnection ? "Auto install" : "JSON",
);
const handleOnNewValue = (value) => {
const flowsMCPData: MCPSettingsType[] = value.value.map((flow) => ({
const handleOnNewValue = (value: any) => {
const flowsMCPData: MCPSettingsType[] = value.value.map((flow: any) => ({
id: flow.id,
action_name: flow.name,
action_description: flow.description,
mcp_enabled: flow.status,
}));
patchFlowsMCP(flowsMCPData);
// Prepare the request with both settings and auth_settings
// If ENABLE_MCP_COMPOSER is false, always use "none" for auth_type
const finalAuthSettings = ENABLE_MCP_COMPOSER
? currentAuthSettings
: { auth_type: "none" };
const requestData = {
settings: flowsMCPData,
auth_settings: finalAuthSettings,
};
patchFlowsMCP(requestData);
};
const handleAuthSave = (authSettings: AuthSettingsType) => {
// Update the current flows with the new auth settings
const flowsMCPData: MCPSettingsType[] =
flowsMCP?.map((flow) => ({
id: flow.id,
action_name: flow.action_name,
action_description: flow.action_description,
mcp_enabled: flow.mcp_enabled,
})) || [];
const requestData = {
settings: flowsMCPData,
auth_settings: authSettings,
};
patchFlowsMCP(requestData);
};
const flowsMCPData = flowsMCP?.map((flow) => ({
@ -187,10 +225,93 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const apiUrl = customGetMCPUrl(projectId);
// Generate auth headers based on the authentication type
const getAuthHeaders = () => {
// If MCP auth is disabled, use the previous API key behavior
if (!ENABLE_MCP_COMPOSER) {
if (isAutoLogin) return "";
return `
"--headers",
"x-api-key",
"${apiKey || "YOUR_API_KEY"}",`;
}
if (!currentAuthSettings || currentAuthSettings.auth_type === "none") {
return "";
}
switch (currentAuthSettings.auth_type) {
case "apikey":
return `
"--headers",
"x-api-key",
"${currentAuthSettings.api_key || "YOUR_API_KEY"}",`;
case "basic":
return `
"--headers",
"Authorization",
"Basic ${btoa(
`${currentAuthSettings.username || "USERNAME"}:${
currentAuthSettings.password || "PASSWORD"
}`,
)}",`;
case "bearer":
return `
"--headers",
"Authorization",
"Bearer ${currentAuthSettings.bearer_token || "YOUR_BEARER_TOKEN"}",`;
case "iam":
return `
"--headers",
"x-api-key",
"${currentAuthSettings.api_key || "YOUR_IAM_TOKEN"}",
"--headers",
"x-iam-endpoint",
"${currentAuthSettings.iam_endpoint || "YOUR_IAM_ENDPOINT"}",`;
case "oauth":
return `
"--auth-type",
"oauth"`;
default:
return "";
}
};
const getEnvVars = () => {
if (!ENABLE_MCP_COMPOSER || currentAuthSettings?.auth_type === "none")
return "";
if (currentAuthSettings?.auth_type === "oauth") {
return `
"env": {
"OAUTH_HOST": "${currentAuthSettings.oauth_host || "YOUR_OAUTH_HOST"}",
"OAUTH_PORT": "${currentAuthSettings.oauth_port || "YOUR_OAUTH_PORT"}",
"OAUTH_SERVER_URL": "${currentAuthSettings.oauth_server_url || "YOUR_OAUTH_SERVER_URL"}",
"OAUTH_CALLBACK_PATH": "${currentAuthSettings.oauth_callback_path || "YOUR_OAUTH_CALLBACK_PATH"}",
"OAUTH_CLIENT_ID": "${currentAuthSettings.oauth_client_id || "YOUR_OAUTH_CLIENT_ID"}",
"OAUTH_CLIENT_SECRET": "${currentAuthSettings.oauth_client_secret || "YOUR_OAUTH_CLIENT_SECRET"}",
"OAUTH_AUTH_URL": "${currentAuthSettings.oauth_auth_url || "YOUR_OAUTH_AUTH_URL"}",
"OAUTH_TOKEN_URL": "${currentAuthSettings.oauth_token_url || "YOUR_OAUTH_TOKEN_URL"}",
"OAUTH_MCP_SCOPE": "${currentAuthSettings.oauth_mcp_scope || "YOUR_OAUTH_MCP_SCOPE"}",
"OAUTH_PROVIDER_SCOPE": "${currentAuthSettings.oauth_provider_scope || "YOUR_OAUTH_PROVIDER_SCOPE"}",
}`;
}
return "";
};
const MCP_SERVER_JSON = `{
"mcpServers": {
"lf-${parseString(folderName ?? "project", ["snake_case", "no_blank", "lowercase"]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
"command": "${selectedPlatform === "windows" ? "cmd" : selectedPlatform === "wsl" ? "wsl" : "uvx"}",
"lf-${parseString(folderName ?? "project", [
"snake_case",
"no_blank",
"lowercase",
]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
"command": "${
selectedPlatform === "windows"
? "cmd"
: selectedPlatform === "wsl"
? "wsl"
: "uvx"
}",
"args": [
${
selectedPlatform === `windows`
@ -201,16 +322,9 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
? `"uvx",
`
: ""
}"mcp-proxy",${
isAutoLogin
? ""
: `
"--headers",
"x-api-key",
"${apiKey || "YOUR_API_KEY"}",`
}
}"${ENABLE_MCP_COMPOSER ? "mcp-composer" : "mcp-proxy"}",${getAuthHeaders()}
"${apiUrl}"
]
]${ENABLE_MCP_COMPOSER && currentAuthSettings?.auth_type === "oauth" ? `,` : ""}${getEnvVars()}
}
}
}`;
@ -239,7 +353,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
.then((res) => {
setApiKey(res["api_key"]);
})
.catch((err) => {})
.catch(() => {})
.finally(() => {
setIsGeneratingApiKey(false);
});
@ -247,42 +361,35 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
const [loadingMCP, setLoadingMCP] = useState<string[]>([]);
// Check if authentication is configured (not "none")
const hasAuthentication =
currentAuthSettings?.auth_type && currentAuthSettings.auth_type !== "none";
return (
<div>
<div className="pb-2 text-sm font-medium" data-testid="mcp-server-title">
MCP Server
</div>
<div className="pb-4 text-mmd text-muted-foreground">
Access your Project's flows as Tools within a MCP Server. Learn more in
our
<a
className="text-accent-pink-foreground"
href={MCP_SERVER_DEPLOY_TUTORIAL_LINK}
target="_blank"
rel="noreferrer"
>
{" "}
Projects as MCP Servers guide.
</a>
<div className="flex justify-between gap-4 items-start">
<div>
<div className="pb-2 font-medium" data-testid="mcp-server-title">
MCP Server
</div>
<div className="pb-4 text-mmd text-muted-foreground">
Access your Project's flows as Tools within a MCP Server. Learn more
in our
<a
className="text-accent-pink-foreground"
href={MCP_SERVER_DEPLOY_TUTORIAL_LINK}
target="_blank"
rel="noreferrer"
>
{" "}
Projects as MCP Servers guide.
</a>
</div>
</div>
</div>
<div className="flex flex-col justify-between gap-8 xl:flex-row">
<div className="w-full xl:w-2/5">
<div className="flex flex-row justify-between">
<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">
<div className="flex flex-row flex-wrap gap-2">
<ToolsComponent
value={flowsMCPData}
title="MCP Server Tools"
@ -297,24 +404,59 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
</div>
</div>
<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">
{[{ name: "Auto install" }, { name: "JSON" }].map(
(item, index) => (
<Button
unstyled
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 ${
selectedMode === item.name
? "border-b-2 border-black dark:border-b-white"
: "text-muted-foreground hover:text-foreground"
} px-3 py-2 text-[13px]`}
onClick={() => setSelectedMode(item.name)}
>
<span>{item.name}</span>
</Button>
),
)}
{[{ name: "Auto install" }, { name: "JSON" }].map((item) => (
<Button
unstyled
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 ${
selectedMode === item.name
? "border-b-2 border-black dark:border-b-white"
: "text-muted-foreground hover:text-foreground"
} px-3 py-2 text-[13px]`}
onClick={() => setSelectedMode(item.name)}
>
<span>{item.name}</span>
</Button>
))}
</div>
</div>
{selectedMode === "JSON" && (
@ -325,10 +467,10 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
onValueChange={setSelectedPlatform}
>
<TabsList>
{operatingSystemTabs.map((tab, index) => (
{operatingSystemTabs.map((tab) => (
<TabsTrigger
className="flex items-center gap-2"
key={index}
key={tab.name}
value={tab.name}
>
<ForwardedIconComponent
@ -394,6 +536,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
)}
{autoInstallers.map((installer) => (
<Button
key={installer.name}
variant="ghost"
className="flex items-center justify-between disabled:text-foreground disabled:opacity-50"
disabled={
@ -461,6 +604,14 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
)}
</div>
</div>
{ENABLE_MCP_COMPOSER && (
<AuthModal
open={authModalOpen}
setOpen={setAuthModalOpen}
authSettings={currentAuthSettings}
onSave={handleAuthSave}
/>
)}
</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 = {
id: string;
mcp_enabled: boolean;
@ -8,6 +27,11 @@ export type MCPSettingsType = {
input_schema?: Record<string, any>;
};
export type MCPProjectResponseType = {
tools: MCPSettingsType[];
auth_settings?: AuthSettingsType;
};
export type MCPServerInfoType = {
id?: string;
name: string;

View file

@ -1,5 +1,28 @@
import { MCPServerType } from "@/types/mcp";
export enum AuthMethodId {
NONE = "none",
API_KEY = "apikey",
BASIC = "basic",
BEARER = "bearer",
IAM = "iam",
OAUTH = "oauth",
}
export const AUTH_METHODS = {
[AuthMethodId.NONE]: { id: AuthMethodId.NONE, label: "None" },
[AuthMethodId.API_KEY]: { id: AuthMethodId.API_KEY, label: "API Key" },
[AuthMethodId.BASIC]: {
id: AuthMethodId.BASIC,
label: "Basic",
},
[AuthMethodId.BEARER]: { id: AuthMethodId.BEARER, label: "Bearer Token" },
[AuthMethodId.IAM]: { id: AuthMethodId.IAM, label: "IAM" },
[AuthMethodId.OAUTH]: { id: AuthMethodId.OAUTH, label: "OAuth" },
} as const;
export const AUTH_METHODS_ARRAY = Object.values(AUTH_METHODS);
/**
* Extracts all MCP servers from a JSON string or object.
* Supports:

View file

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