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_mcp_errors,
|
||||||
handle_read_resource,
|
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.constants import MAX_MCP_SERVER_NAME_LENGTH
|
||||||
from langflow.base.mcp.util import sanitize_mcp_name
|
from langflow.base.mcp.util import sanitize_mcp_name
|
||||||
from langflow.services.database.models import Flow, Folder
|
from langflow.services.database.models import Flow, Folder
|
||||||
from langflow.services.deps import get_settings_service, session_scope
|
from langflow.services.deps import get_settings_service, session_scope
|
||||||
|
from langflow.services.settings.feature_flags import FEATURE_FLAGS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -60,7 +66,7 @@ async def list_project_tools(
|
||||||
current_user: CurrentActiveMCPUser,
|
current_user: CurrentActiveMCPUser,
|
||||||
*,
|
*,
|
||||||
mcp_enabled: bool = True,
|
mcp_enabled: bool = True,
|
||||||
) -> list[MCPSettings]:
|
) -> MCPProjectResponse:
|
||||||
"""List all tools in a project that are enabled for MCP."""
|
"""List all tools in a project that are enabled for MCP."""
|
||||||
tools: list[MCPSettings] = []
|
tools: list[MCPSettings] = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -114,12 +120,19 @@ async def list_project_tools(
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
continue
|
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:
|
except Exception as e:
|
||||||
msg = f"Error listing project tools: {e!s}"
|
msg = f"Error listing project tools: {e!s}"
|
||||||
logger.exception(msg)
|
logger.exception(msg)
|
||||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
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)
|
@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)
|
@router.patch("/{project_id}", status_code=200)
|
||||||
async def update_project_mcp_settings(
|
async def update_project_mcp_settings(
|
||||||
project_id: UUID,
|
project_id: UUID,
|
||||||
settings: list[MCPSettings],
|
request: MCPProjectUpdateRequest,
|
||||||
current_user: CurrentActiveMCPUser,
|
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:
|
try:
|
||||||
async with session_scope() as session:
|
async with session_scope() as session:
|
||||||
# Fetch the project first to verify it exists and belongs to the current user
|
# 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:
|
if not project:
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
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
|
# Query flows in the project
|
||||||
flows = (await session.exec(select(Flow).where(Flow.folder_id == project_id))).all()
|
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 = []
|
updated_flows = []
|
||||||
for flow in flows:
|
for flow in flows:
|
||||||
|
|
@ -256,7 +276,7 @@ async def update_project_mcp_settings(
|
||||||
|
|
||||||
await session.commit()
|
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:
|
except Exception as e:
|
||||||
msg = f"Error updating project MCP settings: {e!s}"
|
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
|
# Determine command and args based on operating system
|
||||||
os_type = platform.system()
|
os_type = platform.system()
|
||||||
command = "uvx"
|
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)
|
# 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()
|
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":
|
if os_type == "Windows":
|
||||||
command = "cmd"
|
command = "cmd"
|
||||||
args = ["/c", "uvx", "mcp-proxy", sse_url]
|
args = ["/c", "uvx", mcp_tool, sse_url]
|
||||||
logger.debug("Windows detected, using cmd command")
|
logger.debug("Windows detected, using cmd command")
|
||||||
|
|
||||||
name = project.name
|
name = project.name
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
ConfigDict,
|
ConfigDict,
|
||||||
Field,
|
Field,
|
||||||
|
SecretStr,
|
||||||
field_serializer,
|
field_serializer,
|
||||||
field_validator,
|
field_validator,
|
||||||
model_serializer,
|
model_serializer,
|
||||||
|
|
@ -440,6 +441,27 @@ class CancelFlowResponse(BaseModel):
|
||||||
message: str
|
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):
|
class MCPSettings(BaseModel):
|
||||||
"""Model representing MCP settings for a flow."""
|
"""Model representing MCP settings for a flow."""
|
||||||
|
|
||||||
|
|
@ -451,5 +473,19 @@ class MCPSettings(BaseModel):
|
||||||
description: str | None = None
|
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):
|
class MCPInstallRequest(BaseModel):
|
||||||
client: str
|
client: str
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from typing import Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import Text, UniqueConstraint
|
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.flow.model import Flow, FlowRead
|
||||||
from langflow.services.database.models.user.model import User
|
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):
|
class FolderBase(SQLModel):
|
||||||
name: str = Field(index=True)
|
name: str = Field(index=True)
|
||||||
description: str | None = Field(default=None, sa_column=Column(Text))
|
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]
|
class Folder(FolderBase, table=True): # type: ignore[call-arg]
|
||||||
|
|
@ -53,3 +58,4 @@ class FolderUpdate(SQLModel):
|
||||||
parent_id: UUID | None = None
|
parent_id: UUID | None = None
|
||||||
components: list[UUID] = Field(default_factory=list)
|
components: list[UUID] = Field(default_factory=list)
|
||||||
flows: 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):
|
class FeatureFlags(BaseSettings):
|
||||||
mvp_components: bool = False
|
mvp_components: bool = False
|
||||||
|
mcp_composer: bool = True
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_prefix = "LANGFLOW_FEATURE_"
|
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."""
|
"""Test successful update of MCP settings using real database."""
|
||||||
# Create settings for updating the flow
|
# Create settings for updating the flow
|
||||||
settings = [
|
json_payload = {
|
||||||
{
|
"settings": [
|
||||||
"id": str(test_flow_for_update.id),
|
{
|
||||||
"action_name": "updated_action",
|
"id": str(test_flow_for_update.id),
|
||||||
"action_description": "Updated description",
|
"action_name": "updated_action",
|
||||||
"mcp_enabled": False,
|
"action_description": "Updated description",
|
||||||
"name": test_flow_for_update.name,
|
"mcp_enabled": False,
|
||||||
"description": test_flow_for_update.description,
|
"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
|
# Make the real PATCH request
|
||||||
response = await client.patch(
|
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
|
# 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
|
# Use real database objects instead of mocks to avoid the coroutine issue
|
||||||
|
|
||||||
# Empty settings list
|
# 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
|
# Make the request to the actual endpoint
|
||||||
response = await client.patch(
|
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
|
# 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."""
|
"""Test that a user can update MCP settings for their own flows using real database."""
|
||||||
# User attempts to update their own flow settings
|
# User attempts to update their own flow settings
|
||||||
updated_settings = [
|
json_payload = {
|
||||||
{
|
"settings": [
|
||||||
"id": str(user_test_flow.id),
|
{
|
||||||
"action_name": "updated_user_action",
|
"id": str(user_test_flow.id),
|
||||||
"action_description": "Updated user action description",
|
"action_name": "updated_user_action",
|
||||||
"mcp_enabled": False,
|
"action_description": "Updated user action description",
|
||||||
"name": "User Test Flow",
|
"mcp_enabled": False,
|
||||||
"description": "This flow belongs to the active user",
|
"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
|
# Make the PATCH request to update settings
|
||||||
response = await client.patch(
|
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
|
# 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-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@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-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slider": "^1.2.1",
|
"@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": {
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
"version": "1.1.10",
|
"version": "1.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
|
"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-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@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-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slider": "^1.2.1",
|
"@radix-ui/react-slider": "^1.2.1",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||||
|
import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags";
|
||||||
import ToolsModal from "@/modals/toolsModal";
|
import ToolsModal from "@/modals/toolsModal";
|
||||||
import { cn, testIdCase } from "@/utils/utils";
|
import { cn, testIdCase } from "@/utils/utils";
|
||||||
import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
|
import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
|
||||||
|
|
@ -59,28 +61,47 @@ export default function ToolsComponent({
|
||||||
icon={icon}
|
icon={icon}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="relative flex w-full items-center gap-3"
|
className="relative flex flex-col w-full gap-3"
|
||||||
data-testid={"div-" + id}
|
data-testid={"div-" + id}
|
||||||
>
|
>
|
||||||
{(visibleActions.length > 0 || isAction) && (
|
<div className="flex flex-row justify-between">
|
||||||
<Button
|
<div className="flex flex-row justify-between">
|
||||||
variant={"ghost"}
|
<ShadTooltip
|
||||||
disabled={!value || disabled}
|
content="Flows in this project can be exposed as callable MCP tools."
|
||||||
size={"iconMd"}
|
side="right"
|
||||||
className={cn(
|
>
|
||||||
"absolute -top-8 right-0 !text-mmd font-normal text-muted-foreground group-hover:text-primary",
|
<div
|
||||||
)}
|
className={cn(
|
||||||
data-testid="button_open_actions"
|
"flex items-center hover:cursor-help",
|
||||||
onClick={() => setIsModalOpen(true)}
|
!ENABLE_MCP_COMPOSER && "text-mmd",
|
||||||
>
|
)}
|
||||||
<ForwardedIconComponent
|
>
|
||||||
name="Settings2"
|
Flows/Tools
|
||||||
className="icon-size"
|
<ForwardedIconComponent
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
name="info"
|
||||||
/>
|
className="ml-1.5 h-4 w-4 text-muted-foreground"
|
||||||
{button_description}
|
aria-hidden="true"
|
||||||
</Button>
|
/>
|
||||||
)}
|
</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 ? (
|
{!value ? (
|
||||||
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
|
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
|
||||||
{[...Array(4)].map((_, index) => (
|
{[...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 { useQueryFunctionType } from "@/types/api";
|
||||||
import type { MCPSettingsType } from "@/types/mcp";
|
import type { MCPProjectResponseType } from "@/types/mcp";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { getURL } from "../../helpers/constants";
|
import { getURL } from "../../helpers/constants";
|
||||||
import { UseRequestProcessor } from "../../services/request-processor";
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
@ -8,7 +8,7 @@ interface IGetFlowsMCP {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type getFlowsMCPResponse = Array<MCPSettingsType>;
|
type getFlowsMCPResponse = MCPProjectResponseType;
|
||||||
|
|
||||||
export const useGetFlowsMCP: useQueryFunctionType<
|
export const useGetFlowsMCP: useQueryFunctionType<
|
||||||
IGetFlowsMCP,
|
IGetFlowsMCP,
|
||||||
|
|
@ -24,7 +24,7 @@ export const useGetFlowsMCP: useQueryFunctionType<
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return [];
|
return { tools: [], auth_settings: undefined };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { UseMutationResult } from "@tanstack/react-query";
|
import type { UseMutationResult } from "@tanstack/react-query";
|
||||||
import type { useMutationFunctionType } from "@/types/api";
|
import type { useMutationFunctionType } from "@/types/api";
|
||||||
import type { MCPSettingsType } from "@/types/mcp";
|
import type { AuthSettingsType, MCPSettingsType } from "@/types/mcp";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { getURL } from "../../helpers/constants";
|
import { getURL } from "../../helpers/constants";
|
||||||
import { UseRequestProcessor } from "../../services/request-processor";
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
@ -9,21 +9,26 @@ interface PatchFlowMCPParams {
|
||||||
project_id: string;
|
project_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PatchFlowMCPRequest {
|
||||||
|
settings: MCPSettingsType[];
|
||||||
|
auth_settings?: AuthSettingsType;
|
||||||
|
}
|
||||||
|
|
||||||
interface PatchFlowMCPResponse {
|
interface PatchFlowMCPResponse {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePatchFlowsMCP: useMutationFunctionType<
|
export const usePatchFlowsMCP: useMutationFunctionType<
|
||||||
PatchFlowMCPParams,
|
PatchFlowMCPParams,
|
||||||
MCPSettingsType[],
|
PatchFlowMCPRequest,
|
||||||
PatchFlowMCPResponse
|
PatchFlowMCPResponse
|
||||||
> = (params, options?) => {
|
> = (params, options?) => {
|
||||||
const { mutate, queryClient } = UseRequestProcessor();
|
const { mutate, queryClient } = UseRequestProcessor();
|
||||||
|
|
||||||
async function patchFlowMCP(flowMCP: MCPSettingsType[]): Promise<any> {
|
async function patchFlowMCP(requestData: PatchFlowMCPRequest): Promise<any> {
|
||||||
const res = await api.patch(
|
const res = await api.patch(
|
||||||
`${getURL("MCP")}/${params.project_id}`,
|
`${getURL("MCP")}/${params.project_id}`,
|
||||||
flowMCP,
|
requestData,
|
||||||
);
|
);
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +36,7 @@ export const usePatchFlowsMCP: useMutationFunctionType<
|
||||||
const mutation: UseMutationResult<
|
const mutation: UseMutationResult<
|
||||||
PatchFlowMCPResponse,
|
PatchFlowMCPResponse,
|
||||||
any,
|
any,
|
||||||
MCPSettingsType[]
|
PatchFlowMCPRequest
|
||||||
> = mutate(["usePatchFlowsMCP"], patchFlowMCP, {
|
> = mutate(["usePatchFlowsMCP"], patchFlowMCP, {
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["useGetFlowsMCP"] });
|
queryClient.refetchQueries({ queryKey: ["useGetFlowsMCP"] });
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,5 @@ export const ENABLE_VOICE_ASSISTANT = true;
|
||||||
export const ENABLE_IMAGE_ON_PLAYGROUND = false;
|
export const ENABLE_IMAGE_ON_PLAYGROUND = false;
|
||||||
export const ENABLE_MCP = true;
|
export const ENABLE_MCP = true;
|
||||||
export const ENABLE_MCP_NOTICE = false;
|
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>
|
</div>
|
||||||
</BaseModal.Header>
|
</BaseModal.Header>
|
||||||
<BaseModal.Content overflowHidden className="flex flex-col p-0">
|
<BaseModal.Content overflowHidden className="flex flex-col p-0">
|
||||||
<div className="flex h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
<SidebarProvider width="20rem" defaultOpen={false}>
|
<div className="flex h-full">
|
||||||
<ToolsTable
|
<SidebarProvider width="20rem" defaultOpen={false}>
|
||||||
rows={rows}
|
<ToolsTable
|
||||||
isAction={isAction}
|
rows={rows}
|
||||||
placeholder={placeholder}
|
isAction={isAction}
|
||||||
data={data}
|
placeholder={placeholder}
|
||||||
setData={setData}
|
data={data}
|
||||||
open={open}
|
setData={setData}
|
||||||
handleOnNewValue={handleOnNewValue}
|
open={open}
|
||||||
/>
|
handleOnNewValue={handleOnNewValue}
|
||||||
</SidebarProvider>
|
/>
|
||||||
|
</SidebarProvider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,16 @@ import {
|
||||||
} from "@/controllers/API/queries/mcp";
|
} from "@/controllers/API/queries/mcp";
|
||||||
import { useGetInstalledMCP } from "@/controllers/API/queries/mcp/use-get-installed-mcp";
|
import { useGetInstalledMCP } from "@/controllers/API/queries/mcp/use-get-installed-mcp";
|
||||||
import { usePatchInstallMCP } from "@/controllers/API/queries/mcp/use-patch-install-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 { useCustomIsLocalConnection } from "@/customization/hooks/use-custom-is-local-connection";
|
||||||
import useTheme from "@/customization/hooks/use-custom-theme";
|
import useTheme from "@/customization/hooks/use-custom-theme";
|
||||||
import { customGetMCPUrl } from "@/customization/utils/custom-mcp-url";
|
import { customGetMCPUrl } from "@/customization/utils/custom-mcp-url";
|
||||||
|
import AuthModal from "@/modals/authModal";
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import useAuthStore from "@/stores/authStore";
|
import useAuthStore from "@/stores/authStore";
|
||||||
import { useFolderStore } from "@/stores/foldersStore";
|
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 { parseString } from "@/utils/stringManipulation";
|
||||||
import { cn, getOS } from "@/utils/utils";
|
import { cn, getOS } from "@/utils/utils";
|
||||||
|
|
||||||
|
|
@ -131,11 +134,16 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
const [apiKey, setApiKey] = useState<string>("");
|
const [apiKey, setApiKey] = useState<string>("");
|
||||||
const [isGeneratingApiKey, setIsGeneratingApiKey] = useState(false);
|
const [isGeneratingApiKey, setIsGeneratingApiKey] = useState(false);
|
||||||
|
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
const { data: flowsMCP } = useGetFlowsMCP({ projectId });
|
const { data: mcpProjectData } = useGetFlowsMCP({ projectId });
|
||||||
const { mutate: patchFlowsMCP } = usePatchFlowsMCP({ project_id: 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({
|
const { mutate: patchInstallMCP } = usePatchInstallMCP({
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
});
|
});
|
||||||
|
|
@ -156,14 +164,44 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
isLocalConnection ? "Auto install" : "JSON",
|
isLocalConnection ? "Auto install" : "JSON",
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnNewValue = (value) => {
|
const handleOnNewValue = (value: any) => {
|
||||||
const flowsMCPData: MCPSettingsType[] = value.value.map((flow) => ({
|
const flowsMCPData: MCPSettingsType[] = value.value.map((flow: any) => ({
|
||||||
id: flow.id,
|
id: flow.id,
|
||||||
action_name: flow.name,
|
action_name: flow.name,
|
||||||
action_description: flow.description,
|
action_description: flow.description,
|
||||||
mcp_enabled: flow.status,
|
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) => ({
|
const flowsMCPData = flowsMCP?.map((flow) => ({
|
||||||
|
|
@ -187,10 +225,93 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
|
|
||||||
const apiUrl = customGetMCPUrl(projectId);
|
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 = `{
|
const MCP_SERVER_JSON = `{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"lf-${parseString(folderName ?? "project", ["snake_case", "no_blank", "lowercase"]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
|
"lf-${parseString(folderName ?? "project", [
|
||||||
"command": "${selectedPlatform === "windows" ? "cmd" : selectedPlatform === "wsl" ? "wsl" : "uvx"}",
|
"snake_case",
|
||||||
|
"no_blank",
|
||||||
|
"lowercase",
|
||||||
|
]).slice(0, MAX_MCP_SERVER_NAME_LENGTH - 4)}": {
|
||||||
|
"command": "${
|
||||||
|
selectedPlatform === "windows"
|
||||||
|
? "cmd"
|
||||||
|
: selectedPlatform === "wsl"
|
||||||
|
? "wsl"
|
||||||
|
: "uvx"
|
||||||
|
}",
|
||||||
"args": [
|
"args": [
|
||||||
${
|
${
|
||||||
selectedPlatform === `windows`
|
selectedPlatform === `windows`
|
||||||
|
|
@ -201,16 +322,9 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
? `"uvx",
|
? `"uvx",
|
||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}"mcp-proxy",${
|
}"${ENABLE_MCP_COMPOSER ? "mcp-composer" : "mcp-proxy"}",${getAuthHeaders()}
|
||||||
isAutoLogin
|
|
||||||
? ""
|
|
||||||
: `
|
|
||||||
"--headers",
|
|
||||||
"x-api-key",
|
|
||||||
"${apiKey || "YOUR_API_KEY"}",`
|
|
||||||
}
|
|
||||||
"${apiUrl}"
|
"${apiUrl}"
|
||||||
]
|
]${ENABLE_MCP_COMPOSER && currentAuthSettings?.auth_type === "oauth" ? `,` : ""}${getEnvVars()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
@ -239,7 +353,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setApiKey(res["api_key"]);
|
setApiKey(res["api_key"]);
|
||||||
})
|
})
|
||||||
.catch((err) => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsGeneratingApiKey(false);
|
setIsGeneratingApiKey(false);
|
||||||
});
|
});
|
||||||
|
|
@ -247,42 +361,35 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
|
|
||||||
const [loadingMCP, setLoadingMCP] = useState<string[]>([]);
|
const [loadingMCP, setLoadingMCP] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Check if authentication is configured (not "none")
|
||||||
|
const hasAuthentication =
|
||||||
|
currentAuthSettings?.auth_type && currentAuthSettings.auth_type !== "none";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="pb-2 text-sm font-medium" data-testid="mcp-server-title">
|
<div className="flex justify-between gap-4 items-start">
|
||||||
MCP Server
|
<div>
|
||||||
</div>
|
<div className="pb-2 font-medium" data-testid="mcp-server-title">
|
||||||
<div className="pb-4 text-mmd text-muted-foreground">
|
MCP Server
|
||||||
Access your Project's flows as Tools within a MCP Server. Learn more in
|
</div>
|
||||||
our
|
<div className="pb-4 text-mmd text-muted-foreground">
|
||||||
<a
|
Access your Project's flows as Tools within a MCP Server. Learn more
|
||||||
className="text-accent-pink-foreground"
|
in our
|
||||||
href={MCP_SERVER_DEPLOY_TUTORIAL_LINK}
|
<a
|
||||||
target="_blank"
|
className="text-accent-pink-foreground"
|
||||||
rel="noreferrer"
|
href={MCP_SERVER_DEPLOY_TUTORIAL_LINK}
|
||||||
>
|
target="_blank"
|
||||||
{" "}
|
rel="noreferrer"
|
||||||
Projects as MCP Servers guide.
|
>
|
||||||
</a>
|
{" "}
|
||||||
|
Projects as MCP Servers guide.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-between gap-8 xl:flex-row">
|
<div className="flex flex-col justify-between gap-8 xl:flex-row">
|
||||||
<div className="w-full xl:w-2/5">
|
<div className="w-full xl:w-2/5">
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
<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">
|
|
||||||
<ToolsComponent
|
<ToolsComponent
|
||||||
value={flowsMCPData}
|
value={flowsMCPData}
|
||||||
title="MCP Server Tools"
|
title="MCP Server Tools"
|
||||||
|
|
@ -297,24 +404,59 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4 overflow-hidden">
|
<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">
|
<div className="flex flex-row justify-start border-b border-border">
|
||||||
{[{ name: "Auto install" }, { name: "JSON" }].map(
|
{[{ name: "Auto install" }, { name: "JSON" }].map((item) => (
|
||||||
(item, index) => (
|
<Button
|
||||||
<Button
|
unstyled
|
||||||
unstyled
|
key={item.name}
|
||||||
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 ${
|
||||||
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
|
||||||
selectedMode === item.name
|
? "border-b-2 border-black dark:border-b-white"
|
||||||
? "border-b-2 border-black dark:border-b-white"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
} px-3 py-2 text-[13px]`}
|
||||||
} px-3 py-2 text-[13px]`}
|
onClick={() => setSelectedMode(item.name)}
|
||||||
onClick={() => setSelectedMode(item.name)}
|
>
|
||||||
>
|
<span>{item.name}</span>
|
||||||
<span>{item.name}</span>
|
</Button>
|
||||||
</Button>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedMode === "JSON" && (
|
{selectedMode === "JSON" && (
|
||||||
|
|
@ -325,10 +467,10 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
onValueChange={setSelectedPlatform}
|
onValueChange={setSelectedPlatform}
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{operatingSystemTabs.map((tab, index) => (
|
{operatingSystemTabs.map((tab) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
key={index}
|
key={tab.name}
|
||||||
value={tab.name}
|
value={tab.name}
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
|
|
@ -394,6 +536,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
)}
|
)}
|
||||||
{autoInstallers.map((installer) => (
|
{autoInstallers.map((installer) => (
|
||||||
<Button
|
<Button
|
||||||
|
key={installer.name}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="flex items-center justify-between disabled:text-foreground disabled:opacity-50"
|
className="flex items-center justify-between disabled:text-foreground disabled:opacity-50"
|
||||||
disabled={
|
disabled={
|
||||||
|
|
@ -461,6 +604,14 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{ENABLE_MCP_COMPOSER && (
|
||||||
|
<AuthModal
|
||||||
|
open={authModalOpen}
|
||||||
|
setOpen={setAuthModalOpen}
|
||||||
|
authSettings={currentAuthSettings}
|
||||||
|
onSave={handleAuthSave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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 = {
|
export type MCPSettingsType = {
|
||||||
id: string;
|
id: string;
|
||||||
mcp_enabled: boolean;
|
mcp_enabled: boolean;
|
||||||
|
|
@ -8,6 +27,11 @@ export type MCPSettingsType = {
|
||||||
input_schema?: Record<string, any>;
|
input_schema?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MCPProjectResponseType = {
|
||||||
|
tools: MCPSettingsType[];
|
||||||
|
auth_settings?: AuthSettingsType;
|
||||||
|
};
|
||||||
|
|
||||||
export type MCPServerInfoType = {
|
export type MCPServerInfoType = {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,28 @@
|
||||||
import { MCPServerType } from "@/types/mcp";
|
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.
|
* Extracts all MCP servers from a JSON string or object.
|
||||||
* Supports:
|
* Supports:
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ export default defineConfig(({ mode }) => {
|
||||||
"process.env.LANGFLOW_AUTO_LOGIN": JSON.stringify(
|
"process.env.LANGFLOW_AUTO_LOGIN": JSON.stringify(
|
||||||
envLangflow.LANGFLOW_AUTO_LOGIN ?? true,
|
envLangflow.LANGFLOW_AUTO_LOGIN ?? true,
|
||||||
),
|
),
|
||||||
|
"process.env.LANGFLOW_FEATURE_MCP_COMPOSER": JSON.stringify(
|
||||||
|
envLangflow.LANGFLOW_FEATURE_MCP_COMPOSER ?? "false",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
plugins: [react(), svgr(), tsconfigPaths()],
|
plugins: [react(), svgr(), tsconfigPaths()],
|
||||||
server: {
|
server: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue