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:
parent
c1d417adf2
commit
9e24202466
19 changed files with 1066 additions and 141 deletions
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
33
src/frontend/package-lock.json
generated
33
src/frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
40
src/frontend/src/components/ui/radio-group.tsx
Normal file
40
src/frontend/src/components/ui/radio-group.tsx
Normal 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 };
|
||||
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] });
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
468
src/frontend/src/modals/authModal/index.tsx
Normal file
468
src/frontend/src/modals/authModal/index.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue