langflow-base-nightly 0.5.0.dev8__py3-none-any.whl → 0.5.0.dev10__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 (152) hide show
  1. langflow/api/v1/mcp.py +20 -284
  2. langflow/api/v1/mcp_projects.py +27 -234
  3. langflow/api/v1/mcp_utils.py +348 -0
  4. langflow/frontend/assets/{SlackIcon-BotWPNKm.js → SlackIcon-BEQsp2tG.js} +1 -1
  5. langflow/frontend/assets/{Wikipedia-DRNSYQow.js → Wikipedia-CylSh2Rz.js} +1 -1
  6. langflow/frontend/assets/{Wolfram-Db_jUSZI.js → Wolfram-BbLCNQCI.js} +1 -1
  7. langflow/frontend/assets/{index-B5Oxgk9P.js → index--afKjQV4.js} +1 -1
  8. langflow/frontend/assets/{index-CVfho6H1.js → index-5ls6VK_c.js} +1 -1
  9. langflow/frontend/assets/{index-BXLF4EEk.js → index-ArQPJTY-.js} +1 -1
  10. langflow/frontend/assets/{index-CDSNOzuw.js → index-B1yGg3ui.js} +2204 -1989
  11. langflow/frontend/assets/index-B2VOPfSQ.js +1 -0
  12. langflow/frontend/assets/{index-BPbfRYEA.js → index-B5S9h8ok.js} +1 -1
  13. langflow/frontend/assets/{index-D-Dm_2dJ.js → index-B61eUMTR.js} +1 -1
  14. langflow/frontend/assets/{index-CQpFrkqi.js → index-B7gZiD-W.js} +1 -1
  15. langflow/frontend/assets/index-BCodFIaO.js +1 -0
  16. langflow/frontend/assets/{index-BBMaOGes.js → index-BDASu5_h.js} +1 -1
  17. langflow/frontend/assets/{index-DClHt8s8.js → index-BRLJm4Du.js} +1 -1
  18. langflow/frontend/assets/{index-B3deLAql.js → index-BSZfrTzR.js} +1 -1
  19. langflow/frontend/assets/{index-C2s1ji9H.js → index-BTPSO7Ty.js} +1 -1
  20. langflow/frontend/assets/{index-BFj0D1r7.js → index-BUepyNwR.js} +1 -1
  21. langflow/frontend/assets/{index-DVc55Ocl.js → index-Bhm1dylP.js} +1 -1
  22. langflow/frontend/assets/index-BjcscQOG.js +1 -0
  23. langflow/frontend/assets/{index-DQXZuPZj.js → index-Bki1gbZ4.js} +1 -1
  24. langflow/frontend/assets/{index-cx7aCRlX.js → index-BlSZbUqf.js} +1 -1
  25. langflow/frontend/assets/{index-CODEJbFb.js → index-BnRJS1jE.js} +1 -1
  26. langflow/frontend/assets/{index-KWI6UqFH.js → index-BorUWqkM.js} +1 -1
  27. langflow/frontend/assets/{index-DTpPH-qx.js → index-Bpv14pFu.js} +1 -1
  28. langflow/frontend/assets/{index-2TJmaEup.js → index-Br2kJx5F.js} +1 -1
  29. langflow/frontend/assets/{index-C2ZbGcjb.js → index-BsVHbutF.js} +1 -1
  30. langflow/frontend/assets/index-BtXZCxsi.js +1 -0
  31. langflow/frontend/assets/{index-D1gs-kle.js → index-Buf0FLlA.js} +1 -1
  32. langflow/frontend/assets/{index-DOGGSk6U.js → index-BwWOFcx9.js} +1 -1
  33. langflow/frontend/assets/{index-CDRdM5TZ.js → index-BxPGc9G_.js} +1 -1
  34. langflow/frontend/assets/{index-B6o6dq6E.js → index-Bxa7Zqaq.js} +1 -1
  35. langflow/frontend/assets/{index-CJvkgxHw.js → index-BymhxBmN.js} +1 -1
  36. langflow/frontend/assets/{index-q3MWMz-i.js → index-C2fMifvM.js} +1 -1
  37. langflow/frontend/assets/{index-C8ndiw7Q.js → index-C3Bj4_FP.js} +1 -1
  38. langflow/frontend/assets/{index-DR9RVbk7.js → index-C9KrGJ67.js} +1 -1
  39. langflow/frontend/assets/{index-BVhxSlDg.js → index-C9ZDIZFW.js} +1 -1
  40. langflow/frontend/assets/{index-DVq9C8tV.js → index-C9dL6qYU.js} +1 -1
  41. langflow/frontend/assets/index-CBGa-ML1.js +1 -0
  42. langflow/frontend/assets/{index-Dz_Mkcgd.js → index-CBOYVi1U.js} +1 -1
  43. langflow/frontend/assets/{index-CF7pTwIL.js → index-CDDBjrKl.js} +1 -1
  44. langflow/frontend/assets/index-CDN9FxX3.js +1 -0
  45. langflow/frontend/assets/{index-DMI44bfI.js → index-CEau7eX7.js} +1 -1
  46. langflow/frontend/assets/{index-DjWYMbHt.js → index-CJ5EUvrn.js} +1 -1
  47. langflow/frontend/assets/index-CSe6UjxV.js +1 -0
  48. langflow/frontend/assets/{index-LwzAiYbw.js → index-CTIjE6ec.js} +1 -1
  49. langflow/frontend/assets/index-C_H9PvPC.js +1 -0
  50. langflow/frontend/assets/{index-CMK4jYBV.js → index-CbN90tiv.js} +1 -1
  51. langflow/frontend/assets/{index-Cq6t6B77.js → index-CcS86fAH.js} +1 -1
  52. langflow/frontend/assets/{index-DqlruG43.js → index-CdPp99Ov.js} +1 -1
  53. langflow/frontend/assets/index-CdaKd40h.js +1 -0
  54. langflow/frontend/assets/{index-CPi2CElO.js → index-ChPNbgSb.js} +1 -1
  55. langflow/frontend/assets/index-ChyV4bY1.js +1 -0
  56. langflow/frontend/assets/{index-CAmO1XSL.js → index-CmPIFu12.js} +1 -1
  57. langflow/frontend/assets/{index-Cb-gNPdm.js → index-CmSCL35l.js} +1 -1
  58. langflow/frontend/assets/index-CoAX4KHA.js +1 -0
  59. langflow/frontend/assets/{index-Ds4x3X2V.js → index-CogD177T.js} +1 -1
  60. langflow/frontend/assets/{index-DiPI-D9B.js → index-Cqc93Aww.js} +1 -1
  61. langflow/frontend/assets/{index-DEEl5v3D.js → index-CtzhOjCJ.js} +1 -1
  62. langflow/frontend/assets/{index-DBZ4408N.js → index-D2K8Dxca.js} +1 -1
  63. langflow/frontend/assets/{index-C2phiJRY.js → index-D38ii1eC.js} +1 -1
  64. langflow/frontend/assets/{index-kFptuRSN.js → index-D6iJ0ixS.js} +1 -1
  65. langflow/frontend/assets/{index-DhTYsc2t.js → index-D6qchtKo.js} +1 -1
  66. langflow/frontend/assets/{index-CRoCIu83.js → index-D8nmnanz.js} +1 -1
  67. langflow/frontend/assets/{index-DkhYUMYL.js → index-DCNa4h3y.js} +1 -1
  68. langflow/frontend/assets/{index-COm8KYOy.js → index-DEwfBIGT.js} +1 -1
  69. langflow/frontend/assets/{index-BOyq956u.js → index-DFckK5jC.js} +1 -1
  70. langflow/frontend/assets/index-DGV6oDTb.js +1 -0
  71. langflow/frontend/assets/{index-CMYQp6pC.js → index-DJzLRh66.js} +1 -1
  72. langflow/frontend/assets/{index-CDHXigRf.js → index-DKho7fub.js} +1 -1
  73. langflow/frontend/assets/{index-b7e24JLR.js → index-DLoNvjqt.js} +1 -1
  74. langflow/frontend/assets/{index-Cc4M6_Fy.js → index-DNOP-INQ.js} +2 -2
  75. langflow/frontend/assets/{index-Bppwxzbg.js → index-DOZ4j-co.js} +1 -1
  76. langflow/frontend/assets/index-DXMYOwee.js +1 -0
  77. langflow/frontend/assets/{index-O2Bsl-oh.js → index-DYj_t4sx.js} +1 -1
  78. langflow/frontend/assets/{index-DtrUiU1_.js → index-DYwMDNLL.js} +1 -1
  79. langflow/frontend/assets/{index-DcjXNb_-.js → index-DZ4RRuo1.js} +1 -1
  80. langflow/frontend/assets/{index-hBPtgyCi.js → index-D_AWHRfe.js} +1 -1
  81. langflow/frontend/assets/{index-kMJ0V4NP.js → index-DaosAMPL.js} +1 -1
  82. langflow/frontend/assets/{index-C4BUzloQ.js → index-DcTv_MVV.js} +1 -1
  83. langflow/frontend/assets/index-DeMmL8Mo.js +1 -0
  84. langflow/frontend/assets/{index-DqbvHtMI.js → index-Dgd8GGgG.js} +1 -1
  85. langflow/frontend/assets/{index-B75wIMbZ.js → index-DgghztJk.js} +1 -1
  86. langflow/frontend/assets/{index-B5EkhN3K.js → index-Dgtl-Ryc.js} +3 -3
  87. langflow/frontend/assets/{index-jPlDcnJ4.js → index-DiYhK7Hs.js} +1 -1
  88. langflow/frontend/assets/{index-DN8sDPsU.js → index-Dj6hJobz.js} +1 -1
  89. langflow/frontend/assets/index-DjNRiQay.js +1 -0
  90. langflow/frontend/assets/{index-BangDkTU.js → index-DmZZmu5_.js} +1 -1
  91. langflow/frontend/assets/index-DpMb4SyP.js +1 -0
  92. langflow/frontend/assets/{index-9eXY-QWQ.js → index-DtjOwLQC.js} +1 -1
  93. langflow/frontend/assets/{index-BdhLcBhs.js → index-DufevlPT.js} +1 -1
  94. langflow/frontend/assets/index-DvZ2dP1K.js +1 -0
  95. langflow/frontend/assets/{index-D1aaBEVW.js → index-DyIpqQ6u.js} +1 -1
  96. langflow/frontend/assets/{index-BsyeSq2y.js → index-DysqOh2v.js} +1 -1
  97. langflow/frontend/assets/{index-DWVXan_1.js → index-DzFPLySG.js} +1 -1
  98. langflow/frontend/assets/{index-B1sswgB8.js → index-GF2LJgkR.js} +1 -1
  99. langflow/frontend/assets/{index-CKRjoqeo.js → index-GUM5ctOD.js} +1 -1
  100. langflow/frontend/assets/{index-CTaTVU6A.js → index-H15pIDqA.js} +1 -1
  101. langflow/frontend/assets/{index-CQI2hSXh.js → index-HbhXQ4iI.js} +1 -1
  102. langflow/frontend/assets/index-KcUnsxlf.js +2 -0
  103. langflow/frontend/assets/{index-eI0i0Az1.js → index-N3NiikE-.js} +1 -1
  104. langflow/frontend/assets/{index-Djk9t5II.js → index-O3c1ky7v.js} +1 -1
  105. langflow/frontend/assets/{index-CiodZmyb.js → index-Q0Ex46_F.js} +1 -1
  106. langflow/frontend/assets/{index-DKsEUeWS.js → index-TUSpXwVe.js} +1 -1
  107. langflow/frontend/assets/{index-CGhiKIDc.js → index-UVKuDcRW.js} +1 -1
  108. langflow/frontend/assets/{index-CfY-quU3.js → index-Vx9vra5E.js} +1 -1
  109. langflow/frontend/assets/{index-D18rYlC2.js → index-YXNg_MqK.js} +1 -1
  110. langflow/frontend/assets/{index-XdytYvV3.js → index-Yrff3uIT.js} +1 -1
  111. langflow/frontend/assets/{index-BbQbwO0t.js → index-ZHQE-way.js} +1 -1
  112. langflow/frontend/assets/{index-y_Xw1u9n.js → index-ZPrp_qyL.js} +1 -1
  113. langflow/frontend/assets/{index-DfskV-9v.js → index-_AhGSFEp.js} +1 -1
  114. langflow/frontend/assets/{index-CMf3qp3F.js → index-cOLi_68F.js} +1 -1
  115. langflow/frontend/assets/{index-BD5Qd3fi.js → index-dg4h4aV9.js} +1 -1
  116. langflow/frontend/assets/{index-DJKnttt_.js → index-fU437Xuh.js} +1 -1
  117. langflow/frontend/assets/{index-ByGM4QOw.js → index-gnvplQfi.js} +1 -1
  118. langflow/frontend/assets/{index-DU3H6Mds.js → index-gsz-bhOF.js} +1 -1
  119. langflow/frontend/assets/{index-C7B7bkdw.js → index-k9UFhzxg.js} +1 -1
  120. langflow/frontend/assets/{index-DM-IghpO.js → index-kjgyrJKk.js} +1 -1
  121. langflow/frontend/assets/index-lJ1V4apH.js +1 -0
  122. langflow/frontend/assets/{index-Cb44GO2-.js → index-lp0TR1VG.js} +1 -1
  123. langflow/frontend/assets/{index-HjrnO1wn.js → index-p77FDSRr.js} +1 -1
  124. langflow/frontend/assets/{index-19eRIFZ4.js → index-pwj4p8sb.js} +1 -1
  125. langflow/frontend/assets/{index-D1RpzgF-.js → index-uxytSC6u.js} +1 -1
  126. langflow/frontend/assets/lazyIconImports-B2rERpGa.js +2 -0
  127. langflow/frontend/assets/{use-post-add-user-e7yXa1cg.js → use-post-add-user-CNxc7cHz.js} +1 -1
  128. langflow/frontend/index.html +1 -1
  129. {langflow_base_nightly-0.5.0.dev8.dist-info → langflow_base_nightly-0.5.0.dev10.dist-info}/METADATA +1 -1
  130. {langflow_base_nightly-0.5.0.dev8.dist-info → langflow_base_nightly-0.5.0.dev10.dist-info}/RECORD +132 -131
  131. langflow/frontend/assets/index-3vqHLc3t.js +0 -1
  132. langflow/frontend/assets/index-B-ES6eS-.js +0 -1
  133. langflow/frontend/assets/index-BOHPjPHg.js +0 -2
  134. langflow/frontend/assets/index-BhqoRytd.js +0 -1
  135. langflow/frontend/assets/index-BkDauS6L.js +0 -1
  136. langflow/frontend/assets/index-BlVoki6h.js +0 -1
  137. langflow/frontend/assets/index-BuasQFB9.js +0 -1
  138. langflow/frontend/assets/index-BwlRbSOp.js +0 -1
  139. langflow/frontend/assets/index-CDkpZKzC.js +0 -1
  140. langflow/frontend/assets/index-CnXzxDXQ.js +0 -1
  141. langflow/frontend/assets/index-Cq19RyYM.js +0 -1
  142. langflow/frontend/assets/index-D-jkcji5.js +0 -1
  143. langflow/frontend/assets/index-DZFxYYFn.js +0 -1
  144. langflow/frontend/assets/index-DlQJ30-N.js +0 -1
  145. langflow/frontend/assets/index-DrK2fUFE.js +0 -1
  146. langflow/frontend/assets/index-GFf8D09h.js +0 -1
  147. langflow/frontend/assets/index-HvyyG2sS.js +0 -1
  148. langflow/frontend/assets/index-uW6TJm8N.js +0 -1
  149. langflow/frontend/assets/index-z7mu-rPB.js +0 -1
  150. langflow/frontend/assets/lazyIconImports-qQ2xBw6p.js +0 -2
  151. {langflow_base_nightly-0.5.0.dev8.dist-info → langflow_base_nightly-0.5.0.dev10.dist-info}/WHEEL +0 -0
  152. {langflow_base_nightly-0.5.0.dev8.dist-info → langflow_base_nightly-0.5.0.dev10.dist-info}/entry_points.txt +0 -0
langflow/api/v1/mcp.py CHANGED
@@ -1,11 +1,4 @@
1
1
  import asyncio
2
- import base64
3
- from collections.abc import Awaitable, Callable
4
- from contextvars import ContextVar
5
- from functools import wraps
6
- from typing import Any, ParamSpec, TypeVar
7
- from urllib.parse import quote, unquote, urlparse
8
- from uuid import uuid4
9
2
 
10
3
  import pydantic
11
4
  from anyio import BrokenResourceError
@@ -15,70 +8,22 @@ from loguru import logger
15
8
  from mcp import types
16
9
  from mcp.server import NotificationOptions, Server
17
10
  from mcp.server.sse import SseServerTransport
18
- from sqlmodel import select
19
11
 
20
12
  from langflow.api.utils import CurrentActiveMCPUser
21
- from langflow.api.v1.endpoints import simple_run_flow
22
- from langflow.api.v1.schemas import SimplifiedAPIRequest
23
- from langflow.base.mcp.constants import MAX_MCP_TOOL_NAME_LENGTH
24
- from langflow.base.mcp.util import get_flow_snake_case, sanitize_mcp_name
25
- from langflow.helpers.flow import json_schema_from_flow
26
- from langflow.schema.message import Message
27
- from langflow.services.database.models.flow.model import Flow
28
- from langflow.services.database.models.user.model import User
29
- from langflow.services.deps import (
30
- get_db_service,
31
- get_settings_service,
32
- get_storage_service,
33
- session_scope,
13
+ from langflow.api.v1.mcp_utils import (
14
+ current_user_ctx,
15
+ handle_call_tool,
16
+ handle_list_resources,
17
+ handle_list_tools,
18
+ handle_mcp_errors,
19
+ handle_read_resource,
34
20
  )
35
- from langflow.services.storage.utils import build_content_type_from_extension
36
-
37
- T = TypeVar("T")
38
- P = ParamSpec("P")
39
-
40
-
41
- def handle_mcp_errors(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
42
- """Decorator to handle MCP endpoint errors consistently."""
43
-
44
- @wraps(func)
45
- async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
46
- try:
47
- return await func(*args, **kwargs)
48
- except Exception as e:
49
- msg = f"Error in {func.__name__}: {e!s}"
50
- logger.exception(msg)
51
- raise
52
-
53
- return wrapper
54
-
55
-
56
- async def with_db_session(operation: Callable[[Any], Awaitable[T]]) -> T:
57
- """Execute an operation within a database session context."""
58
- async with session_scope() as session:
59
- return await operation(session)
60
-
61
-
62
- class MCPConfig:
63
- _instance = None
64
-
65
- def __new__(cls):
66
- if cls._instance is None:
67
- cls._instance = super().__new__(cls)
68
- cls._instance.enable_progress_notifications = None
69
- return cls._instance
70
-
71
-
72
- def get_mcp_config():
73
- return MCPConfig()
74
-
21
+ from langflow.services.deps import get_settings_service
75
22
 
76
23
  router = APIRouter(prefix="/mcp", tags=["mcp"])
77
24
 
78
25
  server = Server("langflow-mcp-server")
79
26
 
80
- # Create a context variable to store the current user
81
- current_user_ctx: ContextVar[User] = ContextVar("current_user_ctx")
82
27
 
83
28
  # Define constants
84
29
  MAX_RETRIES = 2
@@ -94,237 +39,28 @@ async def handle_list_prompts():
94
39
 
95
40
 
96
41
  @server.list_resources()
97
- async def handle_list_resources():
98
- resources = []
99
- try:
100
- db_service = get_db_service()
101
- storage_service = get_storage_service()
102
- settings_service = get_settings_service()
103
-
104
- # Build full URL from settings
105
- host = getattr(settings_service.settings, "host", "localhost")
106
- port = getattr(settings_service.settings, "port", 3000)
107
-
108
- base_url = f"http://{host}:{port}".rstrip("/")
109
-
110
- async with db_service.with_session() as session:
111
- flows = (await session.exec(select(Flow))).all()
112
-
113
- for flow in flows:
114
- if flow.id:
115
- try:
116
- files = await storage_service.list_files(flow_id=str(flow.id))
117
- for file_name in files:
118
- # URL encode the filename
119
- safe_filename = quote(file_name)
120
- resource = types.Resource(
121
- uri=f"{base_url}/api/v1/files/{flow.id}/{safe_filename}",
122
- name=file_name,
123
- description=f"File in flow: {flow.name}",
124
- mimeType=build_content_type_from_extension(file_name),
125
- )
126
- resources.append(resource)
127
- except FileNotFoundError as e:
128
- msg = f"Error listing files for flow {flow.id}: {e}"
129
- logger.debug(msg)
130
- continue
131
- except Exception as e:
132
- msg = f"Error in listing resources: {e!s}"
133
- logger.exception(msg)
134
- raise
135
- return resources
42
+ async def handle_global_resources():
43
+ """Handle listing resources for global MCP server."""
44
+ return await handle_list_resources()
136
45
 
137
46
 
138
47
  @server.read_resource()
139
- async def handle_read_resource(uri: str) -> bytes:
140
- """Handle resource read requests."""
141
- try:
142
- # Parse the URI properly
143
- parsed_uri = urlparse(str(uri))
144
- # Path will be like /api/v1/files/{flow_id}/{filename}
145
- path_parts = parsed_uri.path.split("/")
146
- # Remove empty strings from split
147
- path_parts = [p for p in path_parts if p]
148
-
149
- # The flow_id and filename should be the last two parts
150
- two = 2
151
- if len(path_parts) < two:
152
- msg = f"Invalid URI format: {uri}"
153
- raise ValueError(msg)
154
-
155
- flow_id = path_parts[-2]
156
- filename = unquote(path_parts[-1]) # URL decode the filename
157
-
158
- storage_service = get_storage_service()
159
-
160
- # Read the file content
161
- content = await storage_service.get_file(flow_id=flow_id, file_name=filename)
162
- if not content:
163
- msg = f"File {filename} not found in flow {flow_id}"
164
- raise ValueError(msg)
165
-
166
- # Ensure content is base64 encoded
167
- if isinstance(content, str):
168
- content = content.encode()
169
- return base64.b64encode(content)
170
- except Exception as e:
171
- msg = f"Error reading resource {uri}: {e!s}"
172
- logger.exception(msg)
173
- raise
48
+ async def handle_global_read_resource(uri: str) -> bytes:
49
+ """Handle resource read requests for global MCP server."""
50
+ return await handle_read_resource(uri)
174
51
 
175
52
 
176
53
  @server.list_tools()
177
- async def handle_list_tools():
178
- tools = []
179
- try:
180
- db_service = get_db_service()
181
- async with db_service.with_session() as session:
182
- flows = (await session.exec(select(Flow))).all()
183
-
184
- existing_names = set()
185
- for flow in flows:
186
- if flow.user_id is None:
187
- continue
188
-
189
- base_name = sanitize_mcp_name(flow.name)
190
- name = base_name[:MAX_MCP_TOOL_NAME_LENGTH]
191
- if name in existing_names:
192
- i = 1
193
- while True:
194
- suffix = f"_{i}"
195
- truncated_base = base_name[: MAX_MCP_TOOL_NAME_LENGTH - len(suffix)]
196
- candidate = f"{truncated_base}{suffix}"
197
- if candidate not in existing_names:
198
- name = candidate
199
- break
200
- i += 1
201
- try:
202
- tool = types.Tool(
203
- name=name,
204
- description=f"{flow.id}: {flow.description}"
205
- if flow.description
206
- else f"Tool generated from flow: {name}",
207
- inputSchema=json_schema_from_flow(flow),
208
- )
209
- tools.append(tool)
210
- existing_names.add(name)
211
- except Exception as e: # noqa: BLE001
212
- msg = f"Error in listing tools: {e!s} from flow: {base_name}"
213
- logger.warning(msg)
214
- continue
215
- except Exception as e:
216
- msg = f"Error in listing tools: {e!s}"
217
- logger.exception(msg)
218
- raise
219
- return tools
54
+ async def handle_global_tools():
55
+ """Handle listing tools for global MCP server."""
56
+ return await handle_list_tools()
220
57
 
221
58
 
222
59
  @server.call_tool()
223
60
  @handle_mcp_errors
224
- async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
225
- """Handle tool execution requests."""
226
- mcp_config = get_mcp_config()
227
- if mcp_config.enable_progress_notifications is None:
228
- settings_service = get_settings_service()
229
- mcp_config.enable_progress_notifications = settings_service.settings.mcp_server_enable_progress_notifications
230
-
231
- current_user = current_user_ctx.get()
232
-
233
- async def execute_tool(session):
234
- # get flow id from name
235
- flow = await get_flow_snake_case(name, current_user.id, session)
236
- if not flow:
237
- msg = f"Flow with name '{name}' not found"
238
- raise ValueError(msg)
239
-
240
- # Process inputs
241
- processed_inputs = dict(arguments)
242
-
243
- # Initial progress notification
244
- if mcp_config.enable_progress_notifications and (progress_token := server.request_context.meta.progressToken):
245
- await server.request_context.session.send_progress_notification(
246
- progress_token=progress_token, progress=0.0, total=1.0
247
- )
248
-
249
- conversation_id = str(uuid4())
250
- input_request = SimplifiedAPIRequest(
251
- input_value=processed_inputs.get("input_value", ""), session_id=conversation_id
252
- )
253
-
254
- async def send_progress_updates(progress_token):
255
- try:
256
- progress = 0.0
257
- while True:
258
- await server.request_context.session.send_progress_notification(
259
- progress_token=progress_token, progress=min(0.9, progress), total=1.0
260
- )
261
- progress += 0.1
262
- await asyncio.sleep(1.0)
263
- except asyncio.CancelledError:
264
- if mcp_config.enable_progress_notifications:
265
- await server.request_context.session.send_progress_notification(
266
- progress_token=progress_token, progress=1.0, total=1.0
267
- )
268
- raise
269
-
270
- collected_results = []
271
- try:
272
- progress_task = None
273
- if mcp_config.enable_progress_notifications and server.request_context.meta.progressToken:
274
- progress_task = asyncio.create_task(send_progress_updates(server.request_context.meta.progressToken))
275
-
276
- try:
277
- try:
278
- result = await simple_run_flow(
279
- flow=flow,
280
- input_request=input_request,
281
- stream=False,
282
- api_key_user=current_user,
283
- )
284
- # Process all outputs and messages, ensuring no duplicates
285
- processed_texts = set()
286
-
287
- def add_result(text: str):
288
- if text not in processed_texts:
289
- processed_texts.add(text)
290
- collected_results.append(types.TextContent(type="text", text=text))
291
-
292
- for run_output in result.outputs:
293
- for component_output in run_output.outputs:
294
- # Handle messages
295
- for msg in component_output.messages or []:
296
- add_result(msg.message)
297
- # Handle results
298
- for value in (component_output.results or {}).values():
299
- if isinstance(value, Message):
300
- add_result(value.get_text())
301
- else:
302
- add_result(str(value))
303
- except Exception as e: # noqa: BLE001
304
- error_msg = f"Error Executing the {flow.name} tool. Error: {e!s}"
305
- collected_results.append(types.TextContent(type="text", text=error_msg))
306
-
307
- return collected_results
308
- finally:
309
- if progress_task:
310
- progress_task.cancel()
311
- await asyncio.gather(progress_task, return_exceptions=True)
312
-
313
- except Exception:
314
- if mcp_config.enable_progress_notifications and (
315
- progress_token := server.request_context.meta.progressToken
316
- ):
317
- await server.request_context.session.send_progress_notification(
318
- progress_token=progress_token, progress=1.0, total=1.0
319
- )
320
- raise
321
-
322
- try:
323
- return await with_db_session(execute_tool)
324
- except Exception as e:
325
- msg = f"Error executing tool {name}: {e!s}"
326
- logger.exception(msg)
327
- raise
61
+ async def handle_global_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
62
+ """Handle tool execution requests for global MCP server."""
63
+ return await handle_call_tool(name, arguments, server)
328
64
 
329
65
 
330
66
  sse = SseServerTransport("/api/v1/mcp/")
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import base64
3
2
  import json
4
3
  import logging
5
4
  import os
@@ -10,8 +9,7 @@ from datetime import datetime, timezone
10
9
  from ipaddress import ip_address
11
10
  from pathlib import Path
12
11
  from subprocess import CalledProcessError
13
- from urllib.parse import quote, unquote, urlparse
14
- from uuid import UUID, uuid4
12
+ from uuid import UUID
15
13
 
16
14
  from anyio import BrokenResourceError
17
15
  from fastapi import APIRouter, HTTPException, Request, Response
@@ -23,27 +21,25 @@ from sqlalchemy.orm import selectinload
23
21
  from sqlmodel import select
24
22
 
25
23
  from langflow.api.utils import CurrentActiveMCPUser
26
- from langflow.api.v1.endpoints import simple_run_flow
27
- from langflow.api.v1.mcp import (
24
+ from langflow.api.v1.mcp_utils import (
28
25
  current_user_ctx,
29
- get_mcp_config,
26
+ handle_call_tool,
27
+ handle_list_resources,
28
+ handle_list_tools,
30
29
  handle_mcp_errors,
31
- with_db_session,
30
+ handle_read_resource,
32
31
  )
33
- from langflow.api.v1.schemas import MCPInstallRequest, MCPSettings, SimplifiedAPIRequest
34
- from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH, MAX_MCP_TOOL_NAME_LENGTH
35
- from langflow.base.mcp.util import get_flow_snake_case, get_unique_name, sanitize_mcp_name
36
- from langflow.helpers.flow import json_schema_from_flow
37
- from langflow.schema.message import Message
32
+ from langflow.api.v1.schemas import MCPInstallRequest, MCPSettings
33
+ from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH
34
+ from langflow.base.mcp.util import sanitize_mcp_name
38
35
  from langflow.services.database.models import Flow, Folder
39
- from langflow.services.deps import get_settings_service, get_storage_service, session_scope
40
- from langflow.services.storage.utils import build_content_type_from_extension
36
+ from langflow.services.deps import get_settings_service, session_scope
41
37
 
42
38
  logger = logging.getLogger(__name__)
43
39
 
44
40
  router = APIRouter(prefix="/mcp/project", tags=["mcp_projects"])
45
41
 
46
- # Create a context variable to store the current project
42
+ # Create project-specific context variable
47
43
  current_project_ctx: ContextVar[UUID | None] = ContextVar("current_project_ctx", default=None)
48
44
 
49
45
  # Create a mapping of project-specific SSE transports
@@ -652,236 +648,33 @@ class ProjectMCPServer:
652
648
  @handle_mcp_errors
653
649
  async def handle_list_project_tools():
654
650
  """Handle listing tools for this specific project."""
655
- tools = []
656
- try:
657
- async with session_scope() as session:
658
- # Get flows with mcp_enabled flag set to True and in this project
659
- flows = (
660
- await session.exec(
661
- select(Flow).where(Flow.mcp_enabled == True, Flow.folder_id == self.project_id) # noqa: E712
662
- )
663
- ).all()
664
- existing_names = set()
665
- for flow in flows:
666
- if flow.user_id is None:
667
- continue
668
-
669
- # Use action_name if available, otherwise construct from flow name
670
- base_name = (
671
- sanitize_mcp_name(flow.action_name) if flow.action_name else sanitize_mcp_name(flow.name)
672
- )
673
- name = get_unique_name(base_name, MAX_MCP_TOOL_NAME_LENGTH, existing_names)
674
-
675
- # Use action_description if available, otherwise use defaults
676
- description = flow.action_description or (
677
- flow.description if flow.description else f"Tool generated from flow: {name}"
678
- )
679
-
680
- tool = types.Tool(
681
- name=name,
682
- description=description,
683
- inputSchema=json_schema_from_flow(flow),
684
- )
685
- tools.append(tool)
686
- existing_names.add(name)
687
- except Exception as e: # noqa: BLE001
688
- msg = f"Error in listing project tools: {e!s} from flow: {name}"
689
- logger.warning(msg)
690
- return tools
651
+ return await handle_list_tools(project_id=self.project_id, mcp_enabled_only=True)
691
652
 
692
653
  @self.server.list_prompts()
693
654
  async def handle_list_prompts():
694
655
  return []
695
656
 
696
657
  @self.server.list_resources()
697
- async def handle_list_resources():
698
- resources = []
699
- try:
700
- storage_service = get_storage_service()
701
- settings_service = get_settings_service()
702
-
703
- # Build full URL from settings
704
- host = getattr(settings_service.settings, "host", "localhost")
705
- port = getattr(settings_service.settings, "port", 3000)
706
-
707
- base_url = f"http://{host}:{port}".rstrip("/")
708
-
709
- async with session_scope() as session:
710
- flows = (await session.exec(select(Flow))).all()
711
-
712
- for flow in flows:
713
- if flow.id:
714
- try:
715
- files = await storage_service.list_files(flow_id=str(flow.id))
716
- for file_name in files:
717
- # URL encode the filename
718
- safe_filename = quote(file_name)
719
- resource = types.Resource(
720
- uri=f"{base_url}/api/v1/files/{flow.id}/{safe_filename}",
721
- name=file_name,
722
- description=f"File in flow: {flow.name}",
723
- mimeType=build_content_type_from_extension(file_name),
724
- )
725
- resources.append(resource)
726
- except FileNotFoundError as e:
727
- msg = f"Error listing files for flow {flow.id}: {e}"
728
- logger.debug(msg)
729
- continue
730
- except Exception as e:
731
- msg = f"Error in listing resources: {e!s}"
732
- logger.exception(msg)
733
- raise
734
- return resources
658
+ async def handle_list_project_resources():
659
+ """Handle listing resources for this specific project."""
660
+ return await handle_list_resources(project_id=self.project_id)
735
661
 
736
662
  @self.server.read_resource()
737
- async def handle_read_resource(uri: str) -> bytes:
738
- """Handle resource read requests."""
739
- try:
740
- # Parse the URI properly
741
- parsed_uri = urlparse(str(uri))
742
- # Path will be like /api/v1/files/{flow_id}/{filename}
743
- path_parts = parsed_uri.path.split("/")
744
- # Remove empty strings from split
745
- path_parts = [p for p in path_parts if p]
746
-
747
- # The flow_id and filename should be the last two parts
748
- two = 2
749
- if len(path_parts) < two:
750
- msg = f"Invalid URI format: {uri}"
751
- raise ValueError(msg)
752
-
753
- flow_id = path_parts[-2]
754
- filename = unquote(path_parts[-1]) # URL decode the filename
755
-
756
- storage_service = get_storage_service()
757
-
758
- # Read the file content
759
- content = await storage_service.get_file(flow_id=flow_id, file_name=filename)
760
- if not content:
761
- msg = f"File {filename} not found in flow {flow_id}"
762
- raise ValueError(msg)
763
-
764
- # Ensure content is base64 encoded
765
- if isinstance(content, str):
766
- content = content.encode()
767
- return base64.b64encode(content)
768
- except Exception as e:
769
- msg = f"Error reading resource {uri}: {e!s}"
770
- logger.exception(msg)
771
- raise
663
+ async def handle_read_project_resource(uri: str) -> bytes:
664
+ """Handle resource read requests for this specific project."""
665
+ return await handle_read_resource(uri=uri)
772
666
 
773
667
  @self.server.call_tool()
774
668
  @handle_mcp_errors
775
- async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
776
- """Handle tool execution requests."""
777
- mcp_config = get_mcp_config()
778
- if mcp_config.enable_progress_notifications is None:
779
- settings_service = get_settings_service()
780
- mcp_config.enable_progress_notifications = (
781
- settings_service.settings.mcp_server_enable_progress_notifications
782
- )
783
-
784
- current_user = current_user_ctx.get()
785
-
786
- async def execute_tool(session):
787
- # get flow id from name
788
- flow = await get_flow_snake_case(name, current_user.id, session, is_action=True)
789
- if not flow:
790
- msg = f"Flow with name '{name}' not found"
791
- raise ValueError(msg)
792
-
793
- # Process inputs
794
- processed_inputs = dict(arguments)
795
-
796
- # Initial progress notification
797
- if mcp_config.enable_progress_notifications and (
798
- progress_token := self.server.request_context.meta.progressToken
799
- ):
800
- await self.server.request_context.session.send_progress_notification(
801
- progress_token=progress_token, progress=0.0, total=1.0
802
- )
803
-
804
- conversation_id = str(uuid4())
805
- input_request = SimplifiedAPIRequest(
806
- input_value=processed_inputs.get("input_value", ""), session_id=conversation_id
807
- )
808
-
809
- async def send_progress_updates(progress_token):
810
- try:
811
- progress = 0.0
812
- while True:
813
- await self.server.request_context.session.send_progress_notification(
814
- progress_token=progress_token, progress=min(0.9, progress), total=1.0
815
- )
816
- progress += 0.1
817
- await asyncio.sleep(1.0)
818
- except asyncio.CancelledError:
819
- if mcp_config.enable_progress_notifications:
820
- await self.server.request_context.session.send_progress_notification(
821
- progress_token=progress_token, progress=1.0, total=1.0
822
- )
823
- raise
824
-
825
- collected_results = []
826
- try:
827
- progress_task = None
828
- if mcp_config.enable_progress_notifications and self.server.request_context.meta.progressToken:
829
- progress_task = asyncio.create_task(
830
- send_progress_updates(self.server.request_context.meta.progressToken)
831
- )
832
-
833
- try:
834
- try:
835
- result = await simple_run_flow(
836
- flow=flow,
837
- input_request=input_request,
838
- stream=False,
839
- api_key_user=current_user,
840
- )
841
- # Process all outputs and messages, ensuring no duplicates
842
- processed_texts = set()
843
-
844
- def add_result(text: str):
845
- if text not in processed_texts:
846
- processed_texts.add(text)
847
- collected_results.append(types.TextContent(type="text", text=text))
848
-
849
- for run_output in result.outputs:
850
- for component_output in run_output.outputs:
851
- # Handle messages
852
- for msg in component_output.messages or []:
853
- add_result(msg.message)
854
- # Handle results
855
- for value in (component_output.results or {}).values():
856
- if isinstance(value, Message):
857
- add_result(value.get_text())
858
- else:
859
- add_result(str(value))
860
- except Exception as e: # noqa: BLE001
861
- error_msg = f"Error Executing the {flow.name} tool. Error: {e!s}"
862
- collected_results.append(types.TextContent(type="text", text=error_msg))
863
-
864
- return collected_results
865
- finally:
866
- if progress_task:
867
- progress_task.cancel()
868
- await asyncio.gather(progress_task, return_exceptions=True)
869
-
870
- except Exception:
871
- if mcp_config.enable_progress_notifications and (
872
- progress_token := self.server.request_context.meta.progressToken
873
- ):
874
- await self.server.request_context.session.send_progress_notification(
875
- progress_token=progress_token, progress=1.0, total=1.0
876
- )
877
- raise
878
-
879
- try:
880
- return await with_db_session(execute_tool)
881
- except Exception as e:
882
- msg = f"Error executing tool {name}: {e!s}"
883
- logger.exception(msg)
884
- raise
669
+ async def handle_call_project_tool(name: str, arguments: dict) -> list[types.TextContent]:
670
+ """Handle tool execution requests for this specific project."""
671
+ return await handle_call_tool(
672
+ name=name,
673
+ arguments=arguments,
674
+ server=self.server,
675
+ project_id=self.project_id,
676
+ is_action=True,
677
+ )
885
678
 
886
679
 
887
680
  # Cache of project MCP servers