langflow-base-nightly 0.5.0.dev38__py3-none-any.whl → 0.5.0.dev39__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langflow/alembic/versions/0882f9657f22_encrypt_existing_mcp_auth_settings_.py +122 -0
- langflow/api/v1/mcp_projects.py +373 -52
- langflow/api/v1/schemas.py +1 -2
- langflow/components/FAISS/__init__.py +34 -0
- langflow/components/agents/agent.py +246 -52
- langflow/components/cassandra/__init__.py +40 -0
- langflow/components/chroma/__init__.py +34 -0
- langflow/components/clickhouse/__init__.py +34 -0
- langflow/components/couchbase/__init__.py +34 -0
- langflow/components/datastax/__init__.py +3 -3
- langflow/components/elastic/__init__.py +37 -0
- langflow/components/milvus/__init__.py +34 -0
- langflow/components/mongodb/__init__.py +34 -0
- langflow/components/ollama/ollama.py +1 -0
- langflow/components/perplexity/perplexity.py +3 -13
- langflow/components/pgvector/__init__.py +34 -0
- langflow/components/pinecone/__init__.py +34 -0
- langflow/components/qdrant/__init__.py +34 -0
- langflow/components/redis/__init__.py +36 -2
- langflow/components/redis/redis.py +75 -29
- langflow/components/redis/redis_chat.py +43 -0
- langflow/components/supabase/__init__.py +37 -0
- langflow/components/upstash/__init__.py +34 -0
- langflow/components/vectara/__init__.py +37 -0
- langflow/components/vectorstores/__init__.py +0 -69
- langflow/components/vectorstores/local_db.py +1 -0
- langflow/components/weaviate/__init__.py +34 -0
- langflow/custom/dependency_analyzer.py +165 -0
- langflow/custom/utils.py +34 -16
- langflow/frontend/assets/{SlackIcon-BhW6H3JR.js → SlackIcon-Cr3Q15Px.js} +1 -1
- langflow/frontend/assets/{Wikipedia-Dx5jbiy3.js → Wikipedia-GxM5sPdM.js} +1 -1
- langflow/frontend/assets/{Wolfram-CIyonzwo.js → Wolfram-BN3-VOCA.js} +1 -1
- langflow/frontend/assets/{index-DOEvKC2X.js → index-28oOcafk.js} +1 -1
- langflow/frontend/assets/{index-Bhv79Zso.js → index-2wSXqBtB.js} +1 -1
- langflow/frontend/assets/{index-BRmSeoWR.js → index-3wW7BClE.js} +1 -1
- langflow/frontend/assets/{index-eUkS6iJM.js → index-6pyH3ZJB.js} +1 -1
- langflow/frontend/assets/{index-Cr5v2ave.js → index-AWCSdofD.js} +1 -1
- langflow/frontend/assets/{index-C27Jj_26.js → index-B2Zgv_xv.js} +1 -1
- langflow/frontend/assets/{index-BKKrUElc.js → index-B2ptVQGM.js} +1 -1
- langflow/frontend/assets/{index-BnAFhkSN.js → index-B3TANVes.js} +1 -1
- langflow/frontend/assets/{index-hZUcL0MZ.js → index-B4yCvZKV.js} +1 -1
- langflow/frontend/assets/{index-BPR2mEFC.js → index-BC65VuWx.js} +1 -1
- langflow/frontend/assets/{index-CgU7KF4I.js → index-BCDSei1q.js} +1 -1
- langflow/frontend/assets/{index-CzHzeZuA.js → index-BJy50PvP.js} +1 -1
- langflow/frontend/assets/{index-DkGhPNeA.js → index-BKseQQ2I.js} +1 -1
- langflow/frontend/assets/{index-BVFaF7HW.js → index-BLTxEeTi.js} +1 -1
- langflow/frontend/assets/{index-cEXY6V06.js → index-BRg1f4Mu.js} +1 -1
- langflow/frontend/assets/{index-C2eQmQsn.js → index-BS8Vo8nc.js} +1 -1
- langflow/frontend/assets/{index-gdb7XMS8.js → index-BTKOU4xC.js} +1 -1
- langflow/frontend/assets/{index-U9GWm1eH.js → index-BVwJDmw-.js} +1 -1
- langflow/frontend/assets/{index-BWt5xGeA.js → index-BWYuQ2Sj.js} +1 -1
- langflow/frontend/assets/{index-Dx-Z87KT.js → index-BWdLILDG.js} +1 -1
- langflow/frontend/assets/{index-paQEWYGT.js → index-BZcw4827.js} +1 -1
- langflow/frontend/assets/{index-BDQrd7Tj.js → index-Bbi87Ve4.js} +1 -1
- langflow/frontend/assets/{index-vJOO5U8M.js → index-Bf0IYKLd.js} +1 -1
- langflow/frontend/assets/{index-1Q3VBqKn.js → index-Bg5nrMRh.js} +1 -1
- langflow/frontend/assets/{index-BFQ8KFK0.js → index-BiC280Nx.js} +1 -1
- langflow/frontend/assets/{index-CFNTYfFK.js → index-BiKKN6FR.js} +1 -1
- langflow/frontend/assets/{index-BPfdqCc_.js → index-Bief6eyJ.js} +1 -1
- langflow/frontend/assets/{index-Cxy9sEpy.js → index-BkXec1Yf.js} +1 -1
- langflow/frontend/assets/{index-D4tjMhfY.js → index-Bnl6QHtP.js} +1 -1
- langflow/frontend/assets/{index-BD7Io1hL.js → index-BpxbUiZD.js} +1978 -1978
- langflow/frontend/assets/{index-Ch5r0oW6.js → index-BrJV8psX.js} +1 -1
- langflow/frontend/assets/{index-DOQDkSoK.js → index-BwLWcUXL.js} +1 -1
- langflow/frontend/assets/{index-CMHpjHZl.js → index-Bx7dBY26.js} +1 -1
- langflow/frontend/assets/{index-CbnWRlYY.js → index-C-EdnFdA.js} +1 -1
- langflow/frontend/assets/{index-DljpLeCW.js → index-C-Xfg4cD.js} +1 -1
- langflow/frontend/assets/{index-Bwi4flFg.js → index-C1f2wMat.js} +1 -1
- langflow/frontend/assets/index-C1xroOlH.css +1 -0
- langflow/frontend/assets/{index-D6CSIrp1.js → index-C3KequvP.js} +1 -1
- langflow/frontend/assets/{index-BYjw7Gk3.js → index-C3ZjKdCD.js} +1 -1
- langflow/frontend/assets/{index-DIKUsGLF.js → index-C3l0zYn0.js} +1 -1
- langflow/frontend/assets/{index-CfPBgkqg.js → index-C3yvArUT.js} +1 -1
- langflow/frontend/assets/{index-CsLQiWNf.js → index-C9Cxnkl8.js} +1 -1
- langflow/frontend/assets/{index-mzl9ULw5.js → index-CBc8fEAE.js} +1 -1
- langflow/frontend/assets/{index-CEJNWPhA.js → index-CBvrGgID.js} +1 -1
- langflow/frontend/assets/{index-DwfHWnX7.js → index-CD-PqGCY.js} +1 -1
- langflow/frontend/assets/{index-dyXKnkMi.js → index-CGO1CiUr.js} +1 -1
- langflow/frontend/assets/{index-Dka_Rk4-.js → index-CH5UVA9b.js} +1 -1
- langflow/frontend/assets/{index-uiKla4UR.js → index-CLJeJYjH.js} +1 -1
- langflow/frontend/assets/{index-D9kwEzPB.js → index-CMZ79X-Y.js} +1 -1
- langflow/frontend/assets/{index-BrVhdPZb.js → index-CMzfJKiW.js} +1 -1
- langflow/frontend/assets/{index-Bct1s6__.js → index-CNw1H-Wc.js} +1 -1
- langflow/frontend/assets/{index-B7uEuOPK.js → index-CPHEscq9.js} +1 -1
- langflow/frontend/assets/{index-ekfMOqrF.js → index-CRPKJZw9.js} +1 -1
- langflow/frontend/assets/{index-G4ro0MjT.js → index-CRPyCfYy.js} +1 -1
- langflow/frontend/assets/{index-CSu8KHOi.js → index-CRcMqCIj.js} +1 -1
- langflow/frontend/assets/{index-DsoX2o1S.js → index-CUVDws8F.js} +1 -1
- langflow/frontend/assets/{index-r_8gs4nL.js → index-CVWQfRYZ.js} +1 -1
- langflow/frontend/assets/{index-7hzXChQz.js → index-CVl6MbaM.js} +1 -1
- langflow/frontend/assets/{index-B8UR8v-Q.js → index-CVwWoX99.js} +1 -1
- langflow/frontend/assets/{index-Dda2u_yz.js → index-CWPzZtSx.js} +1 -1
- langflow/frontend/assets/{index-BKeZt2hQ.js → index-CZqRL9DE.js} +1 -1
- langflow/frontend/assets/{index-DHngW1k8.js → index-CdIf07Rw.js} +1 -1
- langflow/frontend/assets/{index-C--IDAyc.js → index-Cewy7JZE.js} +1 -1
- langflow/frontend/assets/{index-DZP_SaHb.js → index-CfwLpbMM.js} +1 -1
- langflow/frontend/assets/{index-CuCM7Wu7.js → index-CiR1dxI4.js} +1 -1
- langflow/frontend/assets/{index-Xi4TplbI.js → index-CiixOzDG.js} +1 -1
- langflow/frontend/assets/{index-BLYw9MK2.js → index-ClsuDmR6.js} +1 -1
- langflow/frontend/assets/{index-DMCWDJOl.js → index-CmEYYRN1.js} +1 -1
- langflow/frontend/assets/{index-CrAF-31Y.js → index-Co20d-eQ.js} +1 -1
- langflow/frontend/assets/{index-DXAfIEvs.js → index-CpzXS6md.js} +1 -1
- langflow/frontend/assets/{index-BmYJJ5YS.js → index-Cqpzl1J4.js} +1 -1
- langflow/frontend/assets/{index-KWY77KfV.js → index-CtVIONP2.js} +1 -1
- langflow/frontend/assets/{index-B3KCdQ91.js → index-CuFXdTx4.js} +1 -1
- langflow/frontend/assets/{index-p2kStSPe.js → index-Cyd2HtHK.js} +1 -1
- langflow/frontend/assets/{index-CkjwSTSM.js → index-D-1tA8Dt.js} +1 -1
- langflow/frontend/assets/{index-BFf0HTFI.js → index-D-KY3kkq.js} +1 -1
- langflow/frontend/assets/{index-BYhcGLTV.js → index-D-_B1a8v.js} +1 -1
- langflow/frontend/assets/{index-Dr6pVDPI.js → index-D14EWPyZ.js} +1 -1
- langflow/frontend/assets/{index-BDuk0d7P.js → index-D2N3l-cw.js} +1 -1
- langflow/frontend/assets/{index-BvGQfVBD.js → index-D5ETnvJa.js} +1 -1
- langflow/frontend/assets/{index-D1oynC8a.js → index-D7kquVv2.js} +1 -1
- langflow/frontend/assets/{index-B1XqWJhG.js → index-DA6-bvgN.js} +1 -1
- langflow/frontend/assets/{index-DzIv3RyR.js → index-DDWBeudF.js} +1 -1
- langflow/frontend/assets/{index-BKlQbl-6.js → index-DDcMAaG4.js} +1 -1
- langflow/frontend/assets/{index-CkK25zZO.js → index-DHgomBdh.js} +1 -1
- langflow/frontend/assets/{index-Bj3lSwvZ.js → index-DJP-ss47.js} +1 -1
- langflow/frontend/assets/{index-DDXsm8tz.js → index-DQ7VYqQc.js} +1 -1
- langflow/frontend/assets/{index-BNQIbda3.js → index-DTqbvGC0.js} +1 -1
- langflow/frontend/assets/{index-BzoRPtTY.js → index-DUpri6zF.js} +1 -1
- langflow/frontend/assets/{index-35sspuLu.js → index-DV3utZDZ.js} +1 -1
- langflow/frontend/assets/{index-BpmqDOeZ.js → index-DXRfN4HV.js} +1 -1
- langflow/frontend/assets/{index-C0E3_MIK.js → index-Db9dYSzy.js} +1 -1
- langflow/frontend/assets/{index-C8K0r39B.js → index-DdtMEn6I.js} +1 -1
- langflow/frontend/assets/{index-BLsVo9iW.js → index-DfDhMHgQ.js} +1 -1
- langflow/frontend/assets/{index-BZFljdMa.js → index-Dfe7qfvf.js} +1 -1
- langflow/frontend/assets/{index-CyP3py8K.js → index-DhtZ5hx8.js} +1 -1
- langflow/frontend/assets/{index-w72fDjpG.js → index-DiB3CTo8.js} +1 -1
- langflow/frontend/assets/{index-CY7_TBTC.js → index-DiGWASY5.js} +1 -1
- langflow/frontend/assets/{index-CmSFKgiD.js → index-Dl5amdBz.js} +1 -1
- langflow/frontend/assets/{index-B0m53xKd.js → index-DlD4dXlZ.js} +1 -1
- langflow/frontend/assets/{index-DnVYJtVO.js → index-DmeiHnfl.js} +1 -1
- langflow/frontend/assets/index-Dmu-X5-4.js +1 -0
- langflow/frontend/assets/{index-CWYiSeWV.js → index-DpVWih90.js} +1 -1
- langflow/frontend/assets/{index-CjsommIr.js → index-DrDrcajG.js} +1 -1
- langflow/frontend/assets/{index-Un9pWxnP.js → index-Du-pc0KE.js} +1 -1
- langflow/frontend/assets/{index-oxHBZk2v.js → index-DwPkMTaY.js} +1 -1
- langflow/frontend/assets/{index-CgwykVGh.js → index-DwQEZe3C.js} +1 -1
- langflow/frontend/assets/{index-BmIx1cws.js → index-DyJFTK24.js} +1 -1
- langflow/frontend/assets/{index-0XQqYgdG.js → index-J38wh62w.js} +1 -1
- langflow/frontend/assets/{index-H7J7w7fa.js → index-Kwdl-e29.js} +1 -1
- langflow/frontend/assets/{index-CUKmGsI6.js → index-OwPvCmpW.js} +1 -1
- langflow/frontend/assets/{index-zV82kQ6k.js → index-Tw3Os-DN.js} +1 -1
- langflow/frontend/assets/{index-8cuhogZP.js → index-X0guhYF8.js} +1 -1
- langflow/frontend/assets/{index-BUse-kxM.js → index-dJWNxIRH.js} +1 -1
- langflow/frontend/assets/{index-DyqITq51.js → index-dcJ8-agu.js} +1 -1
- langflow/frontend/assets/{index-Cg53lrYh.js → index-eo2mAtL-.js} +1 -1
- langflow/frontend/assets/{index-DqbzUcI5.js → index-hG24k5xJ.js} +1 -1
- langflow/frontend/assets/{index-BQrVDjR1.js → index-h_aSZHf3.js} +1 -1
- langflow/frontend/assets/{index-kkA-qHB_.js → index-hbndqB9B.js} +1 -1
- langflow/frontend/assets/{index-DZxUIhWh.js → index-iJngutFo.js} +1 -1
- langflow/frontend/assets/{index-Dg8N3NSO.js → index-lTpteg8t.js} +1 -1
- langflow/frontend/assets/{index-DDhJVVel.js → index-lZX9AvZW.js} +1 -1
- langflow/frontend/assets/{index-BHhnpSkW.js → index-m8QA6VNM.js} +1 -1
- langflow/frontend/assets/{index-Bk4mTwnI.js → index-o0D2S7xW.js} +1 -1
- langflow/frontend/assets/{index-DJESSNJi.js → index-ovFJ_0J6.js} +1 -1
- langflow/frontend/assets/{index-DH6o91_s.js → index-pYJJOcma.js} +1 -1
- langflow/frontend/assets/{index-Bo-ww0Bb.js → index-sI75DsdM.js} +1 -1
- langflow/frontend/assets/{index-BcAgItH4.js → index-xvFOmxx4.js} +1 -1
- langflow/frontend/assets/{index-_cbGmjF4.js → index-z3SRY-mX.js} +1 -1
- langflow/frontend/assets/lazyIconImports-D97HEZkE.js +2 -0
- langflow/frontend/assets/{use-post-add-user-CvtuazTg.js → use-post-add-user-C0MdTpQ5.js} +1 -1
- langflow/frontend/index.html +2 -2
- langflow/graph/graph/base.py +1 -1
- langflow/initial_setup/starter_projects/Basic Prompt Chaining.json +26 -0
- langflow/initial_setup/starter_projects/Basic Prompting.json +26 -0
- langflow/initial_setup/starter_projects/Blog Writer.json +56 -0
- langflow/initial_setup/starter_projects/Custom Component Generator.json +35 -0
- langflow/initial_setup/starter_projects/Document Q&A.json +26 -0
- langflow/initial_setup/starter_projects/Financial Report Parser.json +43 -0
- langflow/initial_setup/starter_projects/Hybrid Search RAG.json +83 -1
- langflow/initial_setup/starter_projects/Image Sentiment Analysis.json +43 -0
- langflow/initial_setup/starter_projects/Instagram Copywriter.json +49 -1
- langflow/initial_setup/starter_projects/Invoice Summarizer.json +40 -1
- langflow/initial_setup/starter_projects/Knowledge Ingestion.json +71 -0
- langflow/initial_setup/starter_projects/Knowledge Retrieval.json +63 -0
- langflow/initial_setup/starter_projects/Market Research.json +57 -1
- langflow/initial_setup/starter_projects/Meeting Summary.json +95 -0
- langflow/initial_setup/starter_projects/Memory Chatbot.json +35 -0
- langflow/initial_setup/starter_projects/News Aggregator.json +61 -1
- langflow/initial_setup/starter_projects/Nvidia Remix.json +67 -2
- langflow/initial_setup/starter_projects/Pok/303/251dex Agent.json" +48 -1
- langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json +43 -0
- langflow/initial_setup/starter_projects/Price Deal Finder.json +53 -1
- langflow/initial_setup/starter_projects/Research Agent.json +40 -1
- langflow/initial_setup/starter_projects/Research Translation Loop.json +66 -0
- langflow/initial_setup/starter_projects/SEO Keyword Generator.json +17 -0
- langflow/initial_setup/starter_projects/SaaS Pricing.json +27 -1
- langflow/initial_setup/starter_projects/Search agent.json +40 -1
- langflow/initial_setup/starter_projects/Sequential Tasks Agents.json +72 -3
- langflow/initial_setup/starter_projects/Simple Agent.json +57 -1
- langflow/initial_setup/starter_projects/Social Media Agent.json +77 -1
- langflow/initial_setup/starter_projects/Text Sentiment Analysis.json +34 -0
- langflow/initial_setup/starter_projects/Travel Planning Agents.json +51 -3
- langflow/initial_setup/starter_projects/Twitter Thread Generator.json +80 -0
- langflow/initial_setup/starter_projects/Vector Store RAG.json +109 -2
- langflow/initial_setup/starter_projects/Youtube Analysis.json +82 -1
- langflow/initial_setup/starter_projects/vector_store_rag.py +1 -1
- langflow/processing/process.py +3 -0
- langflow/services/auth/mcp_encryption.py +104 -0
- langflow/services/settings/feature_flags.py +1 -1
- {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.0.dev39.dist-info}/METADATA +1 -1
- {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.0.dev39.dist-info}/RECORD +229 -211
- langflow/components/vectorstores/redis.py +0 -89
- langflow/frontend/assets/index-BWgIWfv2.js +0 -1
- langflow/frontend/assets/index-CqS7zir1.css +0 -1
- langflow/frontend/assets/lazyIconImports-DTNgvPE-.js +0 -2
- /langflow/components/{vectorstores → FAISS}/faiss.py +0 -0
- /langflow/components/{vectorstores → cassandra}/cassandra.py +0 -0
- /langflow/components/{datastax/cassandra.py → cassandra/cassandra_chat.py} +0 -0
- /langflow/components/{vectorstores → cassandra}/cassandra_graph.py +0 -0
- /langflow/components/{vectorstores → chroma}/chroma.py +0 -0
- /langflow/components/{vectorstores → clickhouse}/clickhouse.py +0 -0
- /langflow/components/{vectorstores → couchbase}/couchbase.py +0 -0
- /langflow/components/{vectorstores → datastax}/astradb.py +0 -0
- /langflow/components/{vectorstores → datastax}/astradb_graph.py +0 -0
- /langflow/components/{vectorstores → datastax}/graph_rag.py +0 -0
- /langflow/components/{vectorstores → datastax}/hcd.py +0 -0
- /langflow/components/{vectorstores → elastic}/elasticsearch.py +0 -0
- /langflow/components/{vectorstores → elastic}/opensearch.py +0 -0
- /langflow/components/{vectorstores → milvus}/milvus.py +0 -0
- /langflow/components/{vectorstores → mongodb}/mongodb_atlas.py +0 -0
- /langflow/components/{vectorstores → pgvector}/pgvector.py +0 -0
- /langflow/components/{vectorstores → pinecone}/pinecone.py +0 -0
- /langflow/components/{vectorstores → qdrant}/qdrant.py +0 -0
- /langflow/components/{vectorstores → supabase}/supabase.py +0 -0
- /langflow/components/{vectorstores → upstash}/upstash.py +0 -0
- /langflow/components/{vectorstores → vectara}/vectara.py +0 -0
- /langflow/components/{vectorstores → vectara}/vectara_rag.py +0 -0
- /langflow/components/{vectorstores → weaviate}/weaviate.py +0 -0
- {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.0.dev39.dist-info}/WHEEL +0 -0
- {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.0.dev39.dist-info}/entry_points.txt +0 -0
langflow/api/v1/mcp_projects.py
CHANGED
|
@@ -8,10 +8,11 @@ from datetime import datetime, timezone
|
|
|
8
8
|
from ipaddress import ip_address
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from subprocess import CalledProcessError
|
|
11
|
+
from typing import Annotated, Any
|
|
11
12
|
from uuid import UUID
|
|
12
13
|
|
|
13
14
|
from anyio import BrokenResourceError
|
|
14
|
-
from fastapi import APIRouter, HTTPException, Request, Response
|
|
15
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
15
16
|
from fastapi.responses import HTMLResponse
|
|
16
17
|
from mcp import types
|
|
17
18
|
from mcp.server import NotificationOptions, Server
|
|
@@ -28,15 +29,122 @@ from langflow.api.v1.mcp_utils import (
|
|
|
28
29
|
handle_mcp_errors,
|
|
29
30
|
handle_read_resource,
|
|
30
31
|
)
|
|
31
|
-
from langflow.api.v1.schemas import
|
|
32
|
+
from langflow.api.v1.schemas import (
|
|
33
|
+
AuthSettings,
|
|
34
|
+
MCPInstallRequest,
|
|
35
|
+
MCPProjectResponse,
|
|
36
|
+
MCPProjectUpdateRequest,
|
|
37
|
+
MCPSettings,
|
|
38
|
+
)
|
|
32
39
|
from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH
|
|
33
40
|
from langflow.base.mcp.util import sanitize_mcp_name
|
|
34
41
|
from langflow.logging import logger
|
|
42
|
+
from langflow.services.auth.mcp_encryption import decrypt_auth_settings, encrypt_auth_settings
|
|
35
43
|
from langflow.services.database.models import Flow, Folder
|
|
44
|
+
from langflow.services.database.models.api_key.crud import check_key, create_api_key
|
|
45
|
+
from langflow.services.database.models.api_key.model import ApiKeyCreate
|
|
46
|
+
from langflow.services.database.models.user.model import User
|
|
36
47
|
from langflow.services.deps import get_settings_service, session_scope
|
|
48
|
+
from langflow.services.settings.feature_flags import FEATURE_FLAGS
|
|
37
49
|
|
|
38
50
|
router = APIRouter(prefix="/mcp/project", tags=["mcp_projects"])
|
|
39
51
|
|
|
52
|
+
|
|
53
|
+
async def verify_project_auth(
|
|
54
|
+
project_id: UUID,
|
|
55
|
+
query_param: str | None = None,
|
|
56
|
+
header_param: str | None = None,
|
|
57
|
+
) -> User:
|
|
58
|
+
"""Custom authentication for MCP project endpoints when API key is required.
|
|
59
|
+
|
|
60
|
+
This is only used when MCP composer is enabled and project requires API key auth.
|
|
61
|
+
"""
|
|
62
|
+
async with session_scope() as session:
|
|
63
|
+
# First, get the project to check its auth settings
|
|
64
|
+
project = (await session.exec(select(Folder).where(Folder.id == project_id))).first()
|
|
65
|
+
|
|
66
|
+
if not project:
|
|
67
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
68
|
+
|
|
69
|
+
# For MCP composer enabled, only use API key
|
|
70
|
+
api_key = query_param or header_param
|
|
71
|
+
if not api_key:
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=401,
|
|
74
|
+
detail="API key required for this project. Provide x-api-key header or query parameter.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Validate the API key
|
|
78
|
+
user = await check_key(session, api_key)
|
|
79
|
+
if not user:
|
|
80
|
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
81
|
+
|
|
82
|
+
# Verify user has access to the project
|
|
83
|
+
project_access = (
|
|
84
|
+
await session.exec(select(Folder).where(Folder.id == project_id, Folder.user_id == user.id))
|
|
85
|
+
).first()
|
|
86
|
+
|
|
87
|
+
if not project_access:
|
|
88
|
+
raise HTTPException(status_code=403, detail="Access denied to this project")
|
|
89
|
+
|
|
90
|
+
return user
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Smart authentication dependency that chooses method based on project settings
|
|
94
|
+
async def verify_project_auth_conditional(
|
|
95
|
+
project_id: UUID,
|
|
96
|
+
request: Request,
|
|
97
|
+
) -> User:
|
|
98
|
+
"""Choose authentication method based on project settings.
|
|
99
|
+
|
|
100
|
+
- MCP Composer enabled + API key auth: Only allow API keys
|
|
101
|
+
- All other cases: Use standard MCP auth (JWT + API keys)
|
|
102
|
+
"""
|
|
103
|
+
async with session_scope() as session:
|
|
104
|
+
# Get project to check auth settings
|
|
105
|
+
project = (await session.exec(select(Folder).where(Folder.id == project_id))).first()
|
|
106
|
+
|
|
107
|
+
if not project:
|
|
108
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
109
|
+
|
|
110
|
+
# Check if this project requires API key only authentication
|
|
111
|
+
if FEATURE_FLAGS.mcp_composer and project.auth_settings:
|
|
112
|
+
auth_settings = AuthSettings(**project.auth_settings)
|
|
113
|
+
if auth_settings.auth_type == "apikey":
|
|
114
|
+
# For MCP composer projects with API key auth, use custom API key validation
|
|
115
|
+
api_key_header_value = request.headers.get("x-api-key")
|
|
116
|
+
api_key_query_value = request.query_params.get("x-api-key")
|
|
117
|
+
return await verify_project_auth(project_id, api_key_query_value, api_key_header_value)
|
|
118
|
+
|
|
119
|
+
# For all other cases, use standard MCP authentication (allows JWT + API keys)
|
|
120
|
+
# Extract token
|
|
121
|
+
token: str | None = None
|
|
122
|
+
auth_header = request.headers.get("authorization")
|
|
123
|
+
if auth_header and auth_header.startswith("Bearer "):
|
|
124
|
+
token = auth_header[7:]
|
|
125
|
+
|
|
126
|
+
# Extract API keys
|
|
127
|
+
api_key_query_value = request.query_params.get("x-api-key")
|
|
128
|
+
api_key_header_value = request.headers.get("x-api-key")
|
|
129
|
+
|
|
130
|
+
# Call the MCP auth function directly
|
|
131
|
+
from langflow.services.auth.utils import get_current_user_mcp
|
|
132
|
+
|
|
133
|
+
user = await get_current_user_mcp(
|
|
134
|
+
token=token or "", query_param=api_key_query_value, header_param=api_key_header_value, db=session
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Verify project access
|
|
138
|
+
project_access = (
|
|
139
|
+
await session.exec(select(Folder).where(Folder.id == project_id, Folder.user_id == user.id))
|
|
140
|
+
).first()
|
|
141
|
+
|
|
142
|
+
if not project_access:
|
|
143
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
144
|
+
|
|
145
|
+
return user
|
|
146
|
+
|
|
147
|
+
|
|
40
148
|
# Create project-specific context variable
|
|
41
149
|
current_project_ctx: ContextVar[UUID | None] = ContextVar("current_project_ctx", default=None)
|
|
42
150
|
|
|
@@ -98,7 +206,7 @@ async def list_project_tools(
|
|
|
98
206
|
)
|
|
99
207
|
try:
|
|
100
208
|
tool = MCPSettings(
|
|
101
|
-
id=
|
|
209
|
+
id=flow.id,
|
|
102
210
|
action_name=name,
|
|
103
211
|
action_description=description,
|
|
104
212
|
mcp_enabled=flow.mcp_enabled,
|
|
@@ -112,12 +220,14 @@ async def list_project_tools(
|
|
|
112
220
|
await logger.awarning(msg)
|
|
113
221
|
continue
|
|
114
222
|
|
|
115
|
-
# Get project-level auth settings
|
|
223
|
+
# Get project-level auth settings and decrypt sensitive fields
|
|
116
224
|
auth_settings = None
|
|
117
225
|
if project.auth_settings:
|
|
118
226
|
from langflow.api.v1.schemas import AuthSettings
|
|
119
227
|
|
|
120
|
-
|
|
228
|
+
# Decrypt sensitive fields before returning
|
|
229
|
+
decrypted_settings = decrypt_auth_settings(project.auth_settings)
|
|
230
|
+
auth_settings = AuthSettings(**decrypted_settings) if decrypted_settings else None
|
|
121
231
|
|
|
122
232
|
except Exception as e:
|
|
123
233
|
msg = f"Error listing project tools: {e!s}"
|
|
@@ -136,18 +246,9 @@ async def im_alive(project_id: str): # noqa: ARG001
|
|
|
136
246
|
async def handle_project_sse(
|
|
137
247
|
project_id: UUID,
|
|
138
248
|
request: Request,
|
|
139
|
-
current_user:
|
|
249
|
+
current_user: Annotated[User, Depends(verify_project_auth_conditional)],
|
|
140
250
|
):
|
|
141
251
|
"""Handle SSE connections for a specific project."""
|
|
142
|
-
# Verify project exists and user has access
|
|
143
|
-
async with session_scope() as session:
|
|
144
|
-
project = (
|
|
145
|
-
await session.exec(select(Folder).where(Folder.id == project_id, Folder.user_id == current_user.id))
|
|
146
|
-
).first()
|
|
147
|
-
|
|
148
|
-
if not project:
|
|
149
|
-
raise HTTPException(status_code=404, detail="Project not found")
|
|
150
|
-
|
|
151
252
|
# Get project-specific SSE transport and MCP server
|
|
152
253
|
sse = get_project_sse(project_id)
|
|
153
254
|
project_server = get_project_mcp_server(project_id)
|
|
@@ -187,17 +288,12 @@ async def handle_project_sse(
|
|
|
187
288
|
|
|
188
289
|
|
|
189
290
|
@router.post("/{project_id}")
|
|
190
|
-
async def handle_project_messages(
|
|
291
|
+
async def handle_project_messages(
|
|
292
|
+
project_id: UUID,
|
|
293
|
+
request: Request,
|
|
294
|
+
current_user: Annotated[User, Depends(verify_project_auth_conditional)],
|
|
295
|
+
):
|
|
191
296
|
"""Handle POST messages for a project-specific MCP server."""
|
|
192
|
-
# Verify project exists and user has access
|
|
193
|
-
async with session_scope() as session:
|
|
194
|
-
project = (
|
|
195
|
-
await session.exec(select(Folder).where(Folder.id == project_id, Folder.user_id == current_user.id))
|
|
196
|
-
).first()
|
|
197
|
-
|
|
198
|
-
if not project:
|
|
199
|
-
raise HTTPException(status_code=404, detail="Project not found")
|
|
200
|
-
|
|
201
297
|
# Set context variables
|
|
202
298
|
user_token = current_user_ctx.set(current_user)
|
|
203
299
|
project_token = current_project_ctx.set(project_id)
|
|
@@ -214,7 +310,11 @@ async def handle_project_messages(project_id: UUID, request: Request, current_us
|
|
|
214
310
|
|
|
215
311
|
|
|
216
312
|
@router.post("/{project_id}/")
|
|
217
|
-
async def handle_project_messages_with_slash(
|
|
313
|
+
async def handle_project_messages_with_slash(
|
|
314
|
+
project_id: UUID,
|
|
315
|
+
request: Request,
|
|
316
|
+
current_user: Annotated[User, Depends(verify_project_auth_conditional)],
|
|
317
|
+
):
|
|
218
318
|
"""Handle POST messages for a project-specific MCP server with trailing slash."""
|
|
219
319
|
# Call the original handler
|
|
220
320
|
return await handle_project_messages(project_id, request, current_user)
|
|
@@ -241,11 +341,33 @@ async def update_project_mcp_settings(
|
|
|
241
341
|
if not project:
|
|
242
342
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
243
343
|
|
|
244
|
-
# Update project-level auth settings
|
|
245
|
-
if request.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
344
|
+
# Update project-level auth settings with encryption
|
|
345
|
+
if "auth_settings" in request.model_fields_set:
|
|
346
|
+
if request.auth_settings is None:
|
|
347
|
+
# Explicitly set to None - clear auth settings
|
|
348
|
+
project.auth_settings = None
|
|
349
|
+
else:
|
|
350
|
+
# Use python mode to get raw values without SecretStr masking
|
|
351
|
+
auth_model = request.auth_settings
|
|
352
|
+
auth_dict = auth_model.model_dump(mode="python", exclude_none=True)
|
|
353
|
+
|
|
354
|
+
# Extract actual secret values before encryption
|
|
355
|
+
from pydantic import SecretStr
|
|
356
|
+
|
|
357
|
+
# Handle api_key if it's a SecretStr
|
|
358
|
+
api_key_val = getattr(auth_model, "api_key", None)
|
|
359
|
+
if isinstance(api_key_val, SecretStr):
|
|
360
|
+
auth_dict["api_key"] = api_key_val.get_secret_value()
|
|
361
|
+
|
|
362
|
+
# Handle oauth_client_secret if it's a SecretStr
|
|
363
|
+
client_secret_val = getattr(auth_model, "oauth_client_secret", None)
|
|
364
|
+
if isinstance(client_secret_val, SecretStr):
|
|
365
|
+
auth_dict["oauth_client_secret"] = client_secret_val.get_secret_value()
|
|
366
|
+
|
|
367
|
+
# Encrypt and store
|
|
368
|
+
encrypted_settings = encrypt_auth_settings(auth_dict)
|
|
369
|
+
project.auth_settings = encrypted_settings
|
|
370
|
+
|
|
249
371
|
session.add(project)
|
|
250
372
|
|
|
251
373
|
# Query flows in the project
|
|
@@ -340,6 +462,7 @@ async def install_mcp_config(
|
|
|
340
462
|
if not is_local_ip(client_ip):
|
|
341
463
|
raise HTTPException(status_code=500, detail="MCP configuration can only be installed from a local connection")
|
|
342
464
|
|
|
465
|
+
removed_servers: list[str] = [] # Track removed servers for reinstallation
|
|
343
466
|
try:
|
|
344
467
|
# Verify project exists and user has access
|
|
345
468
|
async with session_scope() as session:
|
|
@@ -350,6 +473,28 @@ async def install_mcp_config(
|
|
|
350
473
|
if not project:
|
|
351
474
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
352
475
|
|
|
476
|
+
# Check if project requires API key authentication and generate if needed
|
|
477
|
+
generated_api_key = None
|
|
478
|
+
|
|
479
|
+
# Determine if we need to generate an API key based on feature flag
|
|
480
|
+
should_generate_api_key = False
|
|
481
|
+
if not FEATURE_FLAGS.mcp_composer:
|
|
482
|
+
# When MCP_COMPOSER is disabled, only generate API key if autologin is disabled
|
|
483
|
+
# (matches frontend !isAutoLogin check)
|
|
484
|
+
settings_service = get_settings_service()
|
|
485
|
+
should_generate_api_key = not settings_service.auth_settings.AUTO_LOGIN
|
|
486
|
+
elif project.auth_settings:
|
|
487
|
+
# When MCP_COMPOSER is enabled, only generate if auth_type is "apikey"
|
|
488
|
+
auth_settings = AuthSettings(**project.auth_settings) if project.auth_settings else AuthSettings()
|
|
489
|
+
should_generate_api_key = auth_settings.auth_type == "apikey"
|
|
490
|
+
|
|
491
|
+
if should_generate_api_key:
|
|
492
|
+
# Generate API key with specific name format
|
|
493
|
+
api_key_name = f"MCP Project {project.name} - {body.client}"
|
|
494
|
+
api_key_create = ApiKeyCreate(name=api_key_name)
|
|
495
|
+
unmasked_api_key = await create_api_key(session, api_key_create, current_user.id)
|
|
496
|
+
generated_api_key = unmasked_api_key.api_key
|
|
497
|
+
|
|
353
498
|
# Get settings service to build the SSE URL
|
|
354
499
|
settings_service = get_settings_service()
|
|
355
500
|
host = getattr(settings_service.settings, "host", "localhost")
|
|
@@ -380,7 +525,7 @@ async def install_mcp_config(
|
|
|
380
525
|
stdout=asyncio.subprocess.PIPE,
|
|
381
526
|
stderr=asyncio.subprocess.PIPE,
|
|
382
527
|
)
|
|
383
|
-
stdout,
|
|
528
|
+
stdout, _ = await proc.communicate()
|
|
384
529
|
|
|
385
530
|
if proc.returncode == 0 and stdout.strip():
|
|
386
531
|
wsl_ip = stdout.decode().strip().split()[0] # Get first IP address
|
|
@@ -389,8 +534,33 @@ async def install_mcp_config(
|
|
|
389
534
|
sse_url = sse_url.replace(f"http://{host}:{port}", f"http://{wsl_ip}:{port}")
|
|
390
535
|
except OSError as e:
|
|
391
536
|
await logger.awarning("Failed to get WSL IP address: %s. Using default URL.", str(e))
|
|
537
|
+
|
|
538
|
+
# Base args
|
|
539
|
+
args = ["mcp-composer"] if FEATURE_FLAGS.mcp_composer else ["mcp-proxy"]
|
|
540
|
+
|
|
541
|
+
# Add authentication args based on MCP_COMPOSER feature flag and auth settings
|
|
542
|
+
if not FEATURE_FLAGS.mcp_composer:
|
|
543
|
+
# When MCP_COMPOSER is disabled, only use headers format if API key was generated
|
|
544
|
+
# (when autologin is disabled)
|
|
545
|
+
if generated_api_key:
|
|
546
|
+
args.extend(["--headers", "x-api-key", generated_api_key])
|
|
547
|
+
elif project.auth_settings:
|
|
548
|
+
# Decrypt sensitive fields before using them
|
|
549
|
+
decrypted_settings = decrypt_auth_settings(project.auth_settings)
|
|
550
|
+
auth_settings = AuthSettings(**decrypted_settings) if decrypted_settings else AuthSettings()
|
|
551
|
+
args.extend(["--auth_type", auth_settings.auth_type])
|
|
552
|
+
|
|
553
|
+
# When MCP_COMPOSER is enabled, only add headers if auth_type is "apikey"
|
|
554
|
+
auth_settings = AuthSettings(**project.auth_settings)
|
|
555
|
+
if auth_settings.auth_type == "apikey" and generated_api_key:
|
|
556
|
+
args.extend(["--headers", "x-api-key", generated_api_key])
|
|
557
|
+
# If no auth_settings or auth_type is "none", don't add any auth headers
|
|
558
|
+
|
|
559
|
+
# Add the SSE URL
|
|
560
|
+
if FEATURE_FLAGS.mcp_composer:
|
|
561
|
+
args.extend(["--sse-url", sse_url])
|
|
392
562
|
else:
|
|
393
|
-
args
|
|
563
|
+
args.append(sse_url)
|
|
394
564
|
|
|
395
565
|
if os_type == "Windows":
|
|
396
566
|
command = "cmd"
|
|
@@ -400,7 +570,7 @@ async def install_mcp_config(
|
|
|
400
570
|
name = project.name
|
|
401
571
|
|
|
402
572
|
# Create the MCP configuration
|
|
403
|
-
server_config = {
|
|
573
|
+
server_config: dict[str, Any] = {
|
|
404
574
|
"command": command,
|
|
405
575
|
"args": args,
|
|
406
576
|
}
|
|
@@ -487,9 +657,18 @@ async def install_mcp_config(
|
|
|
487
657
|
# If file exists but is invalid JSON, start fresh
|
|
488
658
|
existing_config = {"mcpServers": {}}
|
|
489
659
|
|
|
490
|
-
#
|
|
660
|
+
# Ensure mcpServers section exists
|
|
491
661
|
if "mcpServers" not in existing_config:
|
|
492
662
|
existing_config["mcpServers"] = {}
|
|
663
|
+
|
|
664
|
+
# Remove any existing servers with the same SSE URL (for reinstalling)
|
|
665
|
+
project_sse_url = await get_project_sse_url(project_id)
|
|
666
|
+
existing_config, removed_servers = remove_server_by_sse_url(existing_config, project_sse_url)
|
|
667
|
+
|
|
668
|
+
if removed_servers:
|
|
669
|
+
logger.info("Removed existing MCP servers with same SSE URL for reinstall: %s", removed_servers)
|
|
670
|
+
|
|
671
|
+
# Merge new config with existing config
|
|
493
672
|
existing_config["mcpServers"].update(mcp_config["mcpServers"])
|
|
494
673
|
|
|
495
674
|
# Write the updated config
|
|
@@ -501,7 +680,13 @@ async def install_mcp_config(
|
|
|
501
680
|
await logger.aexception(msg)
|
|
502
681
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
503
682
|
else:
|
|
504
|
-
|
|
683
|
+
action = "reinstalled" if removed_servers else "installed"
|
|
684
|
+
message = f"Successfully {action} MCP configuration for {body.client}"
|
|
685
|
+
if removed_servers:
|
|
686
|
+
message += f" (replaced existing servers: {', '.join(removed_servers)})"
|
|
687
|
+
if generated_api_key:
|
|
688
|
+
auth_type = "API key" if FEATURE_FLAGS.mcp_composer else "legacy API key"
|
|
689
|
+
message += f" with {auth_type} authentication (key name: 'MCP Project {project.name} - {body.client}')"
|
|
505
690
|
await logger.ainfo(message)
|
|
506
691
|
return {"message": message}
|
|
507
692
|
|
|
@@ -522,12 +707,11 @@ async def check_installed_mcp_servers(
|
|
|
522
707
|
if not project:
|
|
523
708
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
524
709
|
|
|
525
|
-
#
|
|
526
|
-
|
|
527
|
-
project_server_name = f"lf-{sanitize_mcp_name(name)[: (MAX_MCP_SERVER_NAME_LENGTH - 4)]}"
|
|
710
|
+
# Generate the SSE URL for this project
|
|
711
|
+
project_sse_url = await get_project_sse_url(project_id)
|
|
528
712
|
|
|
529
713
|
await logger.adebug(
|
|
530
|
-
"Checking for installed MCP servers for project: %s (
|
|
714
|
+
"Checking for installed MCP servers for project: %s (SSE URL: %s)", project.name, project_sse_url
|
|
531
715
|
)
|
|
532
716
|
|
|
533
717
|
# Check configurations for different clients
|
|
@@ -542,13 +726,13 @@ async def check_installed_mcp_servers(
|
|
|
542
726
|
try:
|
|
543
727
|
with cursor_config_path.open("r") as f:
|
|
544
728
|
cursor_config = json.load(f)
|
|
545
|
-
if
|
|
546
|
-
await logger.adebug("Found Cursor config
|
|
729
|
+
if config_contains_sse_url(cursor_config, project_sse_url):
|
|
730
|
+
await logger.adebug("Found Cursor config with matching SSE URL: %s", project_sse_url)
|
|
547
731
|
results.append("cursor")
|
|
548
732
|
else:
|
|
549
733
|
await logger.adebug(
|
|
550
|
-
"Cursor config exists but no
|
|
551
|
-
|
|
734
|
+
"Cursor config exists but no server with SSE URL: %s (available servers: %s)",
|
|
735
|
+
project_sse_url,
|
|
552
736
|
list(cursor_config.get("mcpServers", {}).keys()),
|
|
553
737
|
)
|
|
554
738
|
except json.JSONDecodeError:
|
|
@@ -563,13 +747,13 @@ async def check_installed_mcp_servers(
|
|
|
563
747
|
try:
|
|
564
748
|
with windsurf_config_path.open("r") as f:
|
|
565
749
|
windsurf_config = json.load(f)
|
|
566
|
-
if
|
|
567
|
-
await logger.adebug("Found Windsurf config
|
|
750
|
+
if config_contains_sse_url(windsurf_config, project_sse_url):
|
|
751
|
+
await logger.adebug("Found Windsurf config with matching SSE URL: %s", project_sse_url)
|
|
568
752
|
results.append("windsurf")
|
|
569
753
|
else:
|
|
570
754
|
await logger.adebug(
|
|
571
|
-
"Windsurf config exists but no
|
|
572
|
-
|
|
755
|
+
"Windsurf config exists but no server with SSE URL: %s (available servers: %s)",
|
|
756
|
+
project_sse_url,
|
|
573
757
|
list(windsurf_config.get("mcpServers", {}).keys()),
|
|
574
758
|
)
|
|
575
759
|
except json.JSONDecodeError:
|
|
@@ -631,13 +815,13 @@ async def check_installed_mcp_servers(
|
|
|
631
815
|
try:
|
|
632
816
|
with claude_config_path.open("r") as f:
|
|
633
817
|
claude_config = json.load(f)
|
|
634
|
-
if
|
|
635
|
-
await logger.adebug("Found Claude config
|
|
818
|
+
if config_contains_sse_url(claude_config, project_sse_url):
|
|
819
|
+
await logger.adebug("Found Claude config with matching SSE URL: %s", project_sse_url)
|
|
636
820
|
results.append("claude")
|
|
637
821
|
else:
|
|
638
822
|
await logger.adebug(
|
|
639
|
-
"Claude config exists but no
|
|
640
|
-
|
|
823
|
+
"Claude config exists but no server with SSE URL: %s (available servers: %s)",
|
|
824
|
+
project_sse_url,
|
|
641
825
|
list(claude_config.get("mcpServers", {}).keys()),
|
|
642
826
|
)
|
|
643
827
|
except json.JSONDecodeError:
|
|
@@ -652,6 +836,143 @@ async def check_installed_mcp_servers(
|
|
|
652
836
|
return results
|
|
653
837
|
|
|
654
838
|
|
|
839
|
+
def config_contains_sse_url(config_data: dict, sse_url: str) -> bool:
|
|
840
|
+
"""Check if any MCP server in the config uses the specified SSE URL."""
|
|
841
|
+
mcp_servers = config_data.get("mcpServers", {})
|
|
842
|
+
for server_name, server_config in mcp_servers.items():
|
|
843
|
+
args = server_config.get("args", [])
|
|
844
|
+
# The SSE URL is typically the last argument in mcp-proxy configurations
|
|
845
|
+
if args and args[-1] == sse_url:
|
|
846
|
+
logger.debug("Found matching SSE URL in server: %s", server_name)
|
|
847
|
+
return True
|
|
848
|
+
return False
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
async def get_project_sse_url(project_id: UUID) -> str:
|
|
852
|
+
"""Generate the SSE URL for a project, including WSL handling."""
|
|
853
|
+
# Get settings service to build the SSE URL
|
|
854
|
+
settings_service = get_settings_service()
|
|
855
|
+
host = getattr(settings_service.settings, "host", "localhost")
|
|
856
|
+
port = getattr(settings_service.settings, "port", 3000)
|
|
857
|
+
base_url = f"http://{host}:{port}".rstrip("/")
|
|
858
|
+
project_sse_url = f"{base_url}/api/v1/mcp/project/{project_id}/sse"
|
|
859
|
+
|
|
860
|
+
# Handle WSL case - must match the logic in install function
|
|
861
|
+
os_type = platform.system()
|
|
862
|
+
is_wsl = os_type == "Linux" and "microsoft" in platform.uname().release.lower()
|
|
863
|
+
|
|
864
|
+
if is_wsl and host in {"localhost", "127.0.0.1"}:
|
|
865
|
+
try:
|
|
866
|
+
proc = await create_subprocess_exec(
|
|
867
|
+
"/usr/bin/hostname",
|
|
868
|
+
"-I",
|
|
869
|
+
stdout=asyncio.subprocess.PIPE,
|
|
870
|
+
stderr=asyncio.subprocess.PIPE,
|
|
871
|
+
)
|
|
872
|
+
stdout, stderr = await proc.communicate()
|
|
873
|
+
|
|
874
|
+
if proc.returncode == 0 and stdout.strip():
|
|
875
|
+
wsl_ip = stdout.decode().strip().split()[0] # Get first IP address
|
|
876
|
+
logger.debug("Using WSL IP for external access: %s", wsl_ip)
|
|
877
|
+
# Replace the localhost with the WSL IP in the URL
|
|
878
|
+
project_sse_url = project_sse_url.replace(f"http://{host}:{port}", f"http://{wsl_ip}:{port}")
|
|
879
|
+
except OSError as e:
|
|
880
|
+
logger.warning("Failed to get WSL IP address: %s. Using default URL.", str(e))
|
|
881
|
+
|
|
882
|
+
return project_sse_url
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
async def get_config_path(client: str) -> Path:
|
|
886
|
+
"""Get the configuration file path for a given client and operating system."""
|
|
887
|
+
os_type = platform.system()
|
|
888
|
+
is_wsl = os_type == "Linux" and "microsoft" in platform.uname().release.lower()
|
|
889
|
+
|
|
890
|
+
if client.lower() == "cursor":
|
|
891
|
+
return Path.home() / ".cursor" / "mcp.json"
|
|
892
|
+
if client.lower() == "windsurf":
|
|
893
|
+
return Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
|
|
894
|
+
if client.lower() == "claude":
|
|
895
|
+
if os_type == "Darwin": # macOS
|
|
896
|
+
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
897
|
+
if os_type == "Windows" or is_wsl: # Windows or WSL (Claude runs on Windows host)
|
|
898
|
+
if is_wsl:
|
|
899
|
+
# In WSL, we need to access the Windows APPDATA directory
|
|
900
|
+
try:
|
|
901
|
+
# First try to get the Windows username
|
|
902
|
+
proc = await create_subprocess_exec(
|
|
903
|
+
"/mnt/c/Windows/System32/cmd.exe",
|
|
904
|
+
"/c",
|
|
905
|
+
"echo %USERNAME%",
|
|
906
|
+
stdout=asyncio.subprocess.PIPE,
|
|
907
|
+
stderr=asyncio.subprocess.PIPE,
|
|
908
|
+
)
|
|
909
|
+
stdout, stderr = await proc.communicate()
|
|
910
|
+
|
|
911
|
+
if proc.returncode == 0 and stdout.strip():
|
|
912
|
+
windows_username = stdout.decode().strip()
|
|
913
|
+
return Path(
|
|
914
|
+
f"/mnt/c/Users/{windows_username}/AppData/Roaming/Claude/claude_desktop_config.json"
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
# Fallback: try to find the Windows user directory
|
|
918
|
+
users_dir = Path("/mnt/c/Users")
|
|
919
|
+
if users_dir.exists():
|
|
920
|
+
# Get the first non-system user directory
|
|
921
|
+
user_dirs = [
|
|
922
|
+
d
|
|
923
|
+
for d in users_dir.iterdir()
|
|
924
|
+
if d.is_dir() and not d.name.startswith(("Default", "Public", "All Users"))
|
|
925
|
+
]
|
|
926
|
+
if user_dirs:
|
|
927
|
+
return user_dirs[0] / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
|
|
928
|
+
|
|
929
|
+
if not Path("/mnt/c").exists():
|
|
930
|
+
msg = "Windows C: drive not mounted at /mnt/c in WSL"
|
|
931
|
+
raise ValueError(msg)
|
|
932
|
+
|
|
933
|
+
msg = "Could not find valid Windows user directory in WSL"
|
|
934
|
+
raise ValueError(msg)
|
|
935
|
+
except (OSError, CalledProcessError) as e:
|
|
936
|
+
logger.warning("Failed to determine Windows user path in WSL: %s", str(e))
|
|
937
|
+
msg = f"Could not determine Windows Claude config path in WSL: {e!s}"
|
|
938
|
+
raise ValueError(msg) from e
|
|
939
|
+
# Regular Windows
|
|
940
|
+
return Path(os.environ["APPDATA"]) / "Claude" / "claude_desktop_config.json"
|
|
941
|
+
|
|
942
|
+
msg = "Unsupported operating system for Claude configuration"
|
|
943
|
+
raise ValueError(msg)
|
|
944
|
+
|
|
945
|
+
msg = "Unsupported client"
|
|
946
|
+
raise ValueError(msg)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def remove_server_by_sse_url(config_data: dict, sse_url: str) -> tuple[dict, list[str]]:
|
|
950
|
+
"""Remove any MCP servers that use the specified SSE URL from config data.
|
|
951
|
+
|
|
952
|
+
Returns:
|
|
953
|
+
tuple: (updated_config, list_of_removed_server_names)
|
|
954
|
+
"""
|
|
955
|
+
if "mcpServers" not in config_data:
|
|
956
|
+
return config_data, []
|
|
957
|
+
|
|
958
|
+
removed_servers: list[str] = []
|
|
959
|
+
servers_to_remove: list[str] = []
|
|
960
|
+
|
|
961
|
+
# Find servers to remove
|
|
962
|
+
for server_name, server_config in config_data["mcpServers"].items():
|
|
963
|
+
args = server_config.get("args", [])
|
|
964
|
+
if args and args[-1] == sse_url:
|
|
965
|
+
servers_to_remove.append(server_name)
|
|
966
|
+
|
|
967
|
+
# Remove the servers
|
|
968
|
+
for server_name in servers_to_remove:
|
|
969
|
+
del config_data["mcpServers"][server_name]
|
|
970
|
+
removed_servers.append(server_name)
|
|
971
|
+
logger.debug("Removed existing server with matching SSE URL: %s", server_name)
|
|
972
|
+
|
|
973
|
+
return config_data, removed_servers
|
|
974
|
+
|
|
975
|
+
|
|
655
976
|
# Project-specific MCP server instance for handling project-specific tools
|
|
656
977
|
class ProjectMCPServer:
|
|
657
978
|
def __init__(self, project_id: UUID):
|
langflow/api/v1/schemas.py
CHANGED
|
@@ -445,13 +445,12 @@ class AuthSettings(BaseModel):
|
|
|
445
445
|
"""Model representing authentication settings for MCP."""
|
|
446
446
|
|
|
447
447
|
auth_type: Literal["none", "apikey", "oauth"] = "none"
|
|
448
|
-
api_key: SecretStr | None = None
|
|
449
448
|
oauth_host: str | None = None
|
|
450
449
|
oauth_port: str | None = None
|
|
451
450
|
oauth_server_url: str | None = None
|
|
452
451
|
oauth_callback_path: str | None = None
|
|
453
452
|
oauth_client_id: str | None = None
|
|
454
|
-
oauth_client_secret:
|
|
453
|
+
oauth_client_secret: SecretStr | None = None
|
|
455
454
|
oauth_auth_url: str | None = None
|
|
456
455
|
oauth_token_url: str | None = None
|
|
457
456
|
oauth_mcp_scope: str | None = None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from langflow.components._importing import import_mod
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .faiss import FaissVectorStoreComponent
|
|
9
|
+
|
|
10
|
+
_dynamic_imports = {
|
|
11
|
+
"FaissVectorStoreComponent": "faiss",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FaissVectorStoreComponent",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __getattr__(attr_name: str) -> Any:
|
|
20
|
+
"""Lazily import FAISS components on attribute access."""
|
|
21
|
+
if attr_name not in _dynamic_imports:
|
|
22
|
+
msg = f"module '{__name__}' has no attribute '{attr_name}'"
|
|
23
|
+
raise AttributeError(msg)
|
|
24
|
+
try:
|
|
25
|
+
result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent)
|
|
26
|
+
except (ModuleNotFoundError, ImportError, AttributeError) as e:
|
|
27
|
+
msg = f"Could not import '{attr_name}' from '{__name__}': {e}"
|
|
28
|
+
raise AttributeError(msg) from e
|
|
29
|
+
globals()[attr_name] = result
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def __dir__() -> list[str]:
|
|
34
|
+
return list(__all__)
|