langflow-base-nightly 0.5.0.dev38__py3-none-any.whl → 0.5.1.dev0__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.
Files changed (243) hide show
  1. langflow/alembic/versions/0882f9657f22_encrypt_existing_mcp_auth_settings_.py +122 -0
  2. langflow/api/router.py +2 -0
  3. langflow/api/v1/__init__.py +2 -0
  4. langflow/api/v1/endpoints.py +7 -1
  5. langflow/api/v1/mcp_projects.py +373 -52
  6. langflow/api/v1/openai_responses.py +545 -0
  7. langflow/api/v1/schemas.py +1 -2
  8. langflow/components/FAISS/__init__.py +34 -0
  9. langflow/components/agents/agent.py +246 -52
  10. langflow/components/cassandra/__init__.py +40 -0
  11. langflow/components/chroma/__init__.py +34 -0
  12. langflow/components/clickhouse/__init__.py +34 -0
  13. langflow/components/couchbase/__init__.py +34 -0
  14. langflow/components/data/file.py +302 -376
  15. langflow/components/datastax/__init__.py +3 -3
  16. langflow/components/docling/docling_inline.py +56 -4
  17. langflow/components/elastic/__init__.py +37 -0
  18. langflow/components/milvus/__init__.py +34 -0
  19. langflow/components/mongodb/__init__.py +34 -0
  20. langflow/components/nvidia/nvidia_ingest.py +3 -2
  21. langflow/components/ollama/ollama.py +1 -0
  22. langflow/components/perplexity/perplexity.py +3 -13
  23. langflow/components/pgvector/__init__.py +34 -0
  24. langflow/components/pinecone/__init__.py +34 -0
  25. langflow/components/qdrant/__init__.py +34 -0
  26. langflow/components/redis/__init__.py +36 -2
  27. langflow/components/redis/redis.py +75 -29
  28. langflow/components/redis/redis_chat.py +43 -0
  29. langflow/components/supabase/__init__.py +37 -0
  30. langflow/components/upstash/__init__.py +34 -0
  31. langflow/components/vectara/__init__.py +37 -0
  32. langflow/components/vectorstores/__init__.py +0 -69
  33. langflow/components/vectorstores/local_db.py +1 -0
  34. langflow/components/weaviate/__init__.py +34 -0
  35. langflow/components/youtube/channel.py +1 -1
  36. langflow/custom/custom_component/custom_component.py +11 -0
  37. langflow/custom/dependency_analyzer.py +165 -0
  38. langflow/custom/utils.py +34 -16
  39. langflow/frontend/assets/{SlackIcon-BhW6H3JR.js → SlackIcon-Cr3Q15Px.js} +1 -1
  40. langflow/frontend/assets/{Wikipedia-Dx5jbiy3.js → Wikipedia-GxM5sPdM.js} +1 -1
  41. langflow/frontend/assets/{Wolfram-CIyonzwo.js → Wolfram-BN3-VOCA.js} +1 -1
  42. langflow/frontend/assets/{index-DOEvKC2X.js → index-28oOcafk.js} +1 -1
  43. langflow/frontend/assets/{index-Bhv79Zso.js → index-2wSXqBtB.js} +1 -1
  44. langflow/frontend/assets/{index-BRmSeoWR.js → index-3wW7BClE.js} +1 -1
  45. langflow/frontend/assets/{index-eUkS6iJM.js → index-6pyH3ZJB.js} +1 -1
  46. langflow/frontend/assets/{index-Cr5v2ave.js → index-AWCSdofD.js} +1 -1
  47. langflow/frontend/assets/{index-C27Jj_26.js → index-B2Zgv_xv.js} +1 -1
  48. langflow/frontend/assets/{index-BKKrUElc.js → index-B2ptVQGM.js} +1 -1
  49. langflow/frontend/assets/{index-BnAFhkSN.js → index-B3TANVes.js} +1 -1
  50. langflow/frontend/assets/{index-hZUcL0MZ.js → index-B4yCvZKV.js} +1 -1
  51. langflow/frontend/assets/{index-BPR2mEFC.js → index-BC65VuWx.js} +1 -1
  52. langflow/frontend/assets/{index-CgU7KF4I.js → index-BCDSei1q.js} +1 -1
  53. langflow/frontend/assets/{index-CzHzeZuA.js → index-BJy50PvP.js} +1 -1
  54. langflow/frontend/assets/{index-DkGhPNeA.js → index-BKseQQ2I.js} +1 -1
  55. langflow/frontend/assets/{index-BVFaF7HW.js → index-BLTxEeTi.js} +1 -1
  56. langflow/frontend/assets/{index-cEXY6V06.js → index-BRg1f4Mu.js} +1 -1
  57. langflow/frontend/assets/{index-C2eQmQsn.js → index-BS8Vo8nc.js} +1 -1
  58. langflow/frontend/assets/{index-gdb7XMS8.js → index-BTKOU4xC.js} +1 -1
  59. langflow/frontend/assets/{index-U9GWm1eH.js → index-BVwJDmw-.js} +1 -1
  60. langflow/frontend/assets/{index-BWt5xGeA.js → index-BWYuQ2Sj.js} +1 -1
  61. langflow/frontend/assets/{index-Dx-Z87KT.js → index-BWdLILDG.js} +1 -1
  62. langflow/frontend/assets/{index-paQEWYGT.js → index-BZcw4827.js} +1 -1
  63. langflow/frontend/assets/{index-BDQrd7Tj.js → index-Bbi87Ve4.js} +1 -1
  64. langflow/frontend/assets/{index-vJOO5U8M.js → index-Bf0IYKLd.js} +1 -1
  65. langflow/frontend/assets/{index-1Q3VBqKn.js → index-Bg5nrMRh.js} +1 -1
  66. langflow/frontend/assets/{index-BFQ8KFK0.js → index-BiC280Nx.js} +1 -1
  67. langflow/frontend/assets/{index-CFNTYfFK.js → index-BiKKN6FR.js} +1 -1
  68. langflow/frontend/assets/{index-BPfdqCc_.js → index-Bief6eyJ.js} +1 -1
  69. langflow/frontend/assets/{index-Cxy9sEpy.js → index-BkXec1Yf.js} +1 -1
  70. langflow/frontend/assets/{index-D4tjMhfY.js → index-Bnl6QHtP.js} +1 -1
  71. langflow/frontend/assets/{index-BD7Io1hL.js → index-BpxbUiZD.js} +1978 -1978
  72. langflow/frontend/assets/{index-Ch5r0oW6.js → index-BrJV8psX.js} +1 -1
  73. langflow/frontend/assets/{index-DOQDkSoK.js → index-BwLWcUXL.js} +1 -1
  74. langflow/frontend/assets/{index-CMHpjHZl.js → index-Bx7dBY26.js} +1 -1
  75. langflow/frontend/assets/{index-CbnWRlYY.js → index-C-EdnFdA.js} +1 -1
  76. langflow/frontend/assets/{index-DljpLeCW.js → index-C-Xfg4cD.js} +1 -1
  77. langflow/frontend/assets/{index-Bwi4flFg.js → index-C1f2wMat.js} +1 -1
  78. langflow/frontend/assets/index-C1xroOlH.css +1 -0
  79. langflow/frontend/assets/{index-D6CSIrp1.js → index-C3KequvP.js} +1 -1
  80. langflow/frontend/assets/{index-BYjw7Gk3.js → index-C3ZjKdCD.js} +1 -1
  81. langflow/frontend/assets/{index-DIKUsGLF.js → index-C3l0zYn0.js} +1 -1
  82. langflow/frontend/assets/{index-CfPBgkqg.js → index-C3yvArUT.js} +1 -1
  83. langflow/frontend/assets/{index-CsLQiWNf.js → index-C9Cxnkl8.js} +1 -1
  84. langflow/frontend/assets/{index-mzl9ULw5.js → index-CBc8fEAE.js} +1 -1
  85. langflow/frontend/assets/{index-CEJNWPhA.js → index-CBvrGgID.js} +1 -1
  86. langflow/frontend/assets/{index-DwfHWnX7.js → index-CD-PqGCY.js} +1 -1
  87. langflow/frontend/assets/{index-dyXKnkMi.js → index-CGO1CiUr.js} +1 -1
  88. langflow/frontend/assets/{index-Dka_Rk4-.js → index-CH5UVA9b.js} +1 -1
  89. langflow/frontend/assets/{index-uiKla4UR.js → index-CLJeJYjH.js} +1 -1
  90. langflow/frontend/assets/{index-D9kwEzPB.js → index-CMZ79X-Y.js} +1 -1
  91. langflow/frontend/assets/{index-BrVhdPZb.js → index-CMzfJKiW.js} +1 -1
  92. langflow/frontend/assets/{index-Bct1s6__.js → index-CNw1H-Wc.js} +1 -1
  93. langflow/frontend/assets/{index-B7uEuOPK.js → index-CPHEscq9.js} +1 -1
  94. langflow/frontend/assets/{index-ekfMOqrF.js → index-CRPKJZw9.js} +1 -1
  95. langflow/frontend/assets/{index-G4ro0MjT.js → index-CRPyCfYy.js} +1 -1
  96. langflow/frontend/assets/{index-CSu8KHOi.js → index-CRcMqCIj.js} +1 -1
  97. langflow/frontend/assets/{index-DsoX2o1S.js → index-CUVDws8F.js} +1 -1
  98. langflow/frontend/assets/{index-r_8gs4nL.js → index-CVWQfRYZ.js} +1 -1
  99. langflow/frontend/assets/{index-7hzXChQz.js → index-CVl6MbaM.js} +1 -1
  100. langflow/frontend/assets/{index-B8UR8v-Q.js → index-CVwWoX99.js} +1 -1
  101. langflow/frontend/assets/{index-Dda2u_yz.js → index-CWPzZtSx.js} +1 -1
  102. langflow/frontend/assets/{index-BKeZt2hQ.js → index-CZqRL9DE.js} +1 -1
  103. langflow/frontend/assets/{index-DHngW1k8.js → index-CdIf07Rw.js} +1 -1
  104. langflow/frontend/assets/{index-C--IDAyc.js → index-Cewy7JZE.js} +1 -1
  105. langflow/frontend/assets/{index-DZP_SaHb.js → index-CfwLpbMM.js} +1 -1
  106. langflow/frontend/assets/{index-CuCM7Wu7.js → index-CiR1dxI4.js} +1 -1
  107. langflow/frontend/assets/{index-Xi4TplbI.js → index-CiixOzDG.js} +1 -1
  108. langflow/frontend/assets/{index-BLYw9MK2.js → index-ClsuDmR6.js} +1 -1
  109. langflow/frontend/assets/{index-DMCWDJOl.js → index-CmEYYRN1.js} +1 -1
  110. langflow/frontend/assets/{index-CrAF-31Y.js → index-Co20d-eQ.js} +1 -1
  111. langflow/frontend/assets/{index-DXAfIEvs.js → index-CpzXS6md.js} +1 -1
  112. langflow/frontend/assets/{index-BmYJJ5YS.js → index-Cqpzl1J4.js} +1 -1
  113. langflow/frontend/assets/{index-KWY77KfV.js → index-CtVIONP2.js} +1 -1
  114. langflow/frontend/assets/{index-B3KCdQ91.js → index-CuFXdTx4.js} +1 -1
  115. langflow/frontend/assets/{index-p2kStSPe.js → index-Cyd2HtHK.js} +1 -1
  116. langflow/frontend/assets/{index-CkjwSTSM.js → index-D-1tA8Dt.js} +1 -1
  117. langflow/frontend/assets/{index-BFf0HTFI.js → index-D-KY3kkq.js} +1 -1
  118. langflow/frontend/assets/{index-BYhcGLTV.js → index-D-_B1a8v.js} +1 -1
  119. langflow/frontend/assets/{index-Dr6pVDPI.js → index-D14EWPyZ.js} +1 -1
  120. langflow/frontend/assets/{index-BDuk0d7P.js → index-D2N3l-cw.js} +1 -1
  121. langflow/frontend/assets/{index-BvGQfVBD.js → index-D5ETnvJa.js} +1 -1
  122. langflow/frontend/assets/{index-D1oynC8a.js → index-D7kquVv2.js} +1 -1
  123. langflow/frontend/assets/{index-B1XqWJhG.js → index-DA6-bvgN.js} +1 -1
  124. langflow/frontend/assets/{index-DzIv3RyR.js → index-DDWBeudF.js} +1 -1
  125. langflow/frontend/assets/{index-BKlQbl-6.js → index-DDcMAaG4.js} +1 -1
  126. langflow/frontend/assets/{index-CkK25zZO.js → index-DHgomBdh.js} +1 -1
  127. langflow/frontend/assets/{index-Bj3lSwvZ.js → index-DJP-ss47.js} +1 -1
  128. langflow/frontend/assets/{index-DDXsm8tz.js → index-DQ7VYqQc.js} +1 -1
  129. langflow/frontend/assets/{index-BNQIbda3.js → index-DTqbvGC0.js} +1 -1
  130. langflow/frontend/assets/{index-BzoRPtTY.js → index-DUpri6zF.js} +1 -1
  131. langflow/frontend/assets/{index-35sspuLu.js → index-DV3utZDZ.js} +1 -1
  132. langflow/frontend/assets/{index-BpmqDOeZ.js → index-DXRfN4HV.js} +1 -1
  133. langflow/frontend/assets/{index-C0E3_MIK.js → index-Db9dYSzy.js} +1 -1
  134. langflow/frontend/assets/{index-C8K0r39B.js → index-DdtMEn6I.js} +1 -1
  135. langflow/frontend/assets/{index-BLsVo9iW.js → index-DfDhMHgQ.js} +1 -1
  136. langflow/frontend/assets/{index-BZFljdMa.js → index-Dfe7qfvf.js} +1 -1
  137. langflow/frontend/assets/{index-CyP3py8K.js → index-DhtZ5hx8.js} +1 -1
  138. langflow/frontend/assets/{index-w72fDjpG.js → index-DiB3CTo8.js} +1 -1
  139. langflow/frontend/assets/{index-CY7_TBTC.js → index-DiGWASY5.js} +1 -1
  140. langflow/frontend/assets/{index-CmSFKgiD.js → index-Dl5amdBz.js} +1 -1
  141. langflow/frontend/assets/{index-B0m53xKd.js → index-DlD4dXlZ.js} +1 -1
  142. langflow/frontend/assets/{index-DnVYJtVO.js → index-DmeiHnfl.js} +1 -1
  143. langflow/frontend/assets/index-Dmu-X5-4.js +1 -0
  144. langflow/frontend/assets/{index-CWYiSeWV.js → index-DpVWih90.js} +1 -1
  145. langflow/frontend/assets/{index-CjsommIr.js → index-DrDrcajG.js} +1 -1
  146. langflow/frontend/assets/{index-Un9pWxnP.js → index-Du-pc0KE.js} +1 -1
  147. langflow/frontend/assets/{index-oxHBZk2v.js → index-DwPkMTaY.js} +1 -1
  148. langflow/frontend/assets/{index-CgwykVGh.js → index-DwQEZe3C.js} +1 -1
  149. langflow/frontend/assets/{index-BmIx1cws.js → index-DyJFTK24.js} +1 -1
  150. langflow/frontend/assets/{index-0XQqYgdG.js → index-J38wh62w.js} +1 -1
  151. langflow/frontend/assets/{index-H7J7w7fa.js → index-Kwdl-e29.js} +1 -1
  152. langflow/frontend/assets/{index-CUKmGsI6.js → index-OwPvCmpW.js} +1 -1
  153. langflow/frontend/assets/{index-zV82kQ6k.js → index-Tw3Os-DN.js} +1 -1
  154. langflow/frontend/assets/{index-8cuhogZP.js → index-X0guhYF8.js} +1 -1
  155. langflow/frontend/assets/{index-BUse-kxM.js → index-dJWNxIRH.js} +1 -1
  156. langflow/frontend/assets/{index-DyqITq51.js → index-dcJ8-agu.js} +1 -1
  157. langflow/frontend/assets/{index-Cg53lrYh.js → index-eo2mAtL-.js} +1 -1
  158. langflow/frontend/assets/{index-DqbzUcI5.js → index-hG24k5xJ.js} +1 -1
  159. langflow/frontend/assets/{index-BQrVDjR1.js → index-h_aSZHf3.js} +1 -1
  160. langflow/frontend/assets/{index-kkA-qHB_.js → index-hbndqB9B.js} +1 -1
  161. langflow/frontend/assets/{index-DZxUIhWh.js → index-iJngutFo.js} +1 -1
  162. langflow/frontend/assets/{index-Dg8N3NSO.js → index-lTpteg8t.js} +1 -1
  163. langflow/frontend/assets/{index-DDhJVVel.js → index-lZX9AvZW.js} +1 -1
  164. langflow/frontend/assets/{index-BHhnpSkW.js → index-m8QA6VNM.js} +1 -1
  165. langflow/frontend/assets/{index-Bk4mTwnI.js → index-o0D2S7xW.js} +1 -1
  166. langflow/frontend/assets/{index-DJESSNJi.js → index-ovFJ_0J6.js} +1 -1
  167. langflow/frontend/assets/{index-DH6o91_s.js → index-pYJJOcma.js} +1 -1
  168. langflow/frontend/assets/{index-Bo-ww0Bb.js → index-sI75DsdM.js} +1 -1
  169. langflow/frontend/assets/{index-BcAgItH4.js → index-xvFOmxx4.js} +1 -1
  170. langflow/frontend/assets/{index-_cbGmjF4.js → index-z3SRY-mX.js} +1 -1
  171. langflow/frontend/assets/lazyIconImports-D97HEZkE.js +2 -0
  172. langflow/frontend/assets/{use-post-add-user-CvtuazTg.js → use-post-add-user-C0MdTpQ5.js} +1 -1
  173. langflow/frontend/index.html +2 -2
  174. langflow/graph/graph/base.py +4 -2
  175. langflow/initial_setup/starter_projects/Basic Prompt Chaining.json +26 -0
  176. langflow/initial_setup/starter_projects/Basic Prompting.json +26 -0
  177. langflow/initial_setup/starter_projects/Blog Writer.json +56 -0
  178. langflow/initial_setup/starter_projects/Custom Component Generator.json +35 -0
  179. langflow/initial_setup/starter_projects/Document Q&A.json +27 -1
  180. langflow/initial_setup/starter_projects/Financial Report Parser.json +43 -0
  181. langflow/initial_setup/starter_projects/Hybrid Search RAG.json +83 -1
  182. langflow/initial_setup/starter_projects/Image Sentiment Analysis.json +43 -0
  183. langflow/initial_setup/starter_projects/Instagram Copywriter.json +49 -1
  184. langflow/initial_setup/starter_projects/Invoice Summarizer.json +40 -1
  185. langflow/initial_setup/starter_projects/Knowledge Ingestion.json +71 -0
  186. langflow/initial_setup/starter_projects/Knowledge Retrieval.json +63 -0
  187. langflow/initial_setup/starter_projects/Market Research.json +57 -1
  188. langflow/initial_setup/starter_projects/Meeting Summary.json +95 -0
  189. langflow/initial_setup/starter_projects/Memory Chatbot.json +35 -0
  190. langflow/initial_setup/starter_projects/News Aggregator.json +61 -1
  191. langflow/initial_setup/starter_projects/Nvidia Remix.json +67 -2
  192. langflow/initial_setup/starter_projects/Pok/303/251dex Agent.json" +48 -1
  193. langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json +44 -1
  194. langflow/initial_setup/starter_projects/Price Deal Finder.json +53 -1
  195. langflow/initial_setup/starter_projects/Research Agent.json +40 -1
  196. langflow/initial_setup/starter_projects/Research Translation Loop.json +66 -0
  197. langflow/initial_setup/starter_projects/SEO Keyword Generator.json +17 -0
  198. langflow/initial_setup/starter_projects/SaaS Pricing.json +27 -1
  199. langflow/initial_setup/starter_projects/Search agent.json +40 -1
  200. langflow/initial_setup/starter_projects/Sequential Tasks Agents.json +72 -3
  201. langflow/initial_setup/starter_projects/Simple Agent.json +57 -1
  202. langflow/initial_setup/starter_projects/Social Media Agent.json +77 -1
  203. langflow/initial_setup/starter_projects/Text Sentiment Analysis.json +35 -1
  204. langflow/initial_setup/starter_projects/Travel Planning Agents.json +51 -3
  205. langflow/initial_setup/starter_projects/Twitter Thread Generator.json +80 -0
  206. langflow/initial_setup/starter_projects/Vector Store RAG.json +110 -3
  207. langflow/initial_setup/starter_projects/Youtube Analysis.json +82 -1
  208. langflow/initial_setup/starter_projects/vector_store_rag.py +1 -1
  209. langflow/processing/process.py +3 -0
  210. langflow/schema/openai_responses_schemas.py +74 -0
  211. langflow/services/auth/mcp_encryption.py +104 -0
  212. langflow/services/settings/feature_flags.py +1 -1
  213. {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.1.dev0.dist-info}/METADATA +1 -1
  214. {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.1.dev0.dist-info}/RECORD +239 -219
  215. langflow/components/vectorstores/redis.py +0 -89
  216. langflow/frontend/assets/index-BWgIWfv2.js +0 -1
  217. langflow/frontend/assets/index-CqS7zir1.css +0 -1
  218. langflow/frontend/assets/lazyIconImports-DTNgvPE-.js +0 -2
  219. /langflow/components/{vectorstores → FAISS}/faiss.py +0 -0
  220. /langflow/components/{vectorstores → cassandra}/cassandra.py +0 -0
  221. /langflow/components/{datastax/cassandra.py → cassandra/cassandra_chat.py} +0 -0
  222. /langflow/components/{vectorstores → cassandra}/cassandra_graph.py +0 -0
  223. /langflow/components/{vectorstores → chroma}/chroma.py +0 -0
  224. /langflow/components/{vectorstores → clickhouse}/clickhouse.py +0 -0
  225. /langflow/components/{vectorstores → couchbase}/couchbase.py +0 -0
  226. /langflow/components/{vectorstores → datastax}/astradb.py +0 -0
  227. /langflow/components/{vectorstores → datastax}/astradb_graph.py +0 -0
  228. /langflow/components/{vectorstores → datastax}/graph_rag.py +0 -0
  229. /langflow/components/{vectorstores → datastax}/hcd.py +0 -0
  230. /langflow/components/{vectorstores → elastic}/elasticsearch.py +0 -0
  231. /langflow/components/{vectorstores → elastic}/opensearch.py +0 -0
  232. /langflow/components/{vectorstores → milvus}/milvus.py +0 -0
  233. /langflow/components/{vectorstores → mongodb}/mongodb_atlas.py +0 -0
  234. /langflow/components/{vectorstores → pgvector}/pgvector.py +0 -0
  235. /langflow/components/{vectorstores → pinecone}/pinecone.py +0 -0
  236. /langflow/components/{vectorstores → qdrant}/qdrant.py +0 -0
  237. /langflow/components/{vectorstores → supabase}/supabase.py +0 -0
  238. /langflow/components/{vectorstores → upstash}/upstash.py +0 -0
  239. /langflow/components/{vectorstores → vectara}/vectara.py +0 -0
  240. /langflow/components/{vectorstores → vectara}/vectara_rag.py +0 -0
  241. /langflow/components/{vectorstores → weaviate}/weaviate.py +0 -0
  242. {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.1.dev0.dist-info}/WHEEL +0 -0
  243. {langflow_base_nightly-0.5.0.dev38.dist-info → langflow_base_nightly-0.5.1.dev0.dist-info}/entry_points.txt +0 -0
@@ -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 MCPInstallRequest, MCPProjectResponse, MCPProjectUpdateRequest, MCPSettings
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=str(flow.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
- auth_settings = AuthSettings(**project.auth_settings)
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: CurrentActiveMCPUser,
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(project_id: UUID, request: Request, current_user: CurrentActiveMCPUser):
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(project_id: UUID, request: Request, current_user: CurrentActiveMCPUser):
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.auth_settings:
246
- project.auth_settings = request.auth_settings.model_dump(mode="json")
247
- else:
248
- project.auth_settings = None
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, stderr = await proc.communicate()
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 = ["mcp-proxy", sse_url]
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
- # Merge new config with existing config
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
- message = f"Successfully installed MCP configuration for {body.client}"
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
- # Project server name pattern (must match the logic in install function)
526
- name = project.name
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 (server name: %s)", project.name, project_server_name
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 "mcpServers" in cursor_config and project_server_name in cursor_config["mcpServers"]:
546
- await logger.adebug("Found Cursor config for project server: %s", project_server_name)
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 entry for server: %s (available servers: %s)",
551
- project_server_name,
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 "mcpServers" in windsurf_config and project_server_name in windsurf_config["mcpServers"]:
567
- await logger.adebug("Found Windsurf config for project server: %s", project_server_name)
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 entry for server: %s (available servers: %s)",
572
- project_server_name,
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 "mcpServers" in claude_config and project_server_name in claude_config["mcpServers"]:
635
- await logger.adebug("Found Claude config for project server: %s", project_server_name)
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 entry for server: %s (available servers: %s)",
640
- project_server_name,
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):