langflow-base-nightly 0.5.0.dev9__py3-none-any.whl → 0.5.0.dev11__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/api/v1/mcp.py +20 -284
- langflow/api/v1/mcp_projects.py +27 -234
- langflow/api/v1/mcp_utils.py +348 -0
- langflow/custom/utils.py +49 -3
- langflow/frontend/assets/{SlackIcon-DZ1aT_Zf.js → SlackIcon-BEQsp2tG.js} +1 -1
- langflow/frontend/assets/{Wikipedia-D1MF1xHp.js → Wikipedia-CylSh2Rz.js} +1 -1
- langflow/frontend/assets/{Wolfram-Bu1y6eWN.js → Wolfram-BbLCNQCI.js} +1 -1
- langflow/frontend/assets/{index-Ck5CNf7-.js → index--afKjQV4.js} +1 -1
- langflow/frontend/assets/{index-CESbYylK.js → index-5ls6VK_c.js} +1 -1
- langflow/frontend/assets/{index-BojmIHoo.js → index-ArQPJTY-.js} +1 -1
- langflow/frontend/assets/{index-BJH-sZAv.js → index-B1yGg3ui.js} +2204 -1989
- langflow/frontend/assets/{index-CnTMIPba.js → index-B2VOPfSQ.js} +1 -1
- langflow/frontend/assets/{index-CqB3cR5l.js → index-B5S9h8ok.js} +1 -1
- langflow/frontend/assets/{index-BBlKT0Bz.js → index-B61eUMTR.js} +1 -1
- langflow/frontend/assets/{index-CZOxlvZN.js → index-B7gZiD-W.js} +1 -1
- langflow/frontend/assets/{index-Cc56TqLJ.js → index-BCodFIaO.js} +1 -1
- langflow/frontend/assets/{index-6_J_Q3ej.js → index-BDASu5_h.js} +1 -1
- langflow/frontend/assets/{index-Bo7mehm9.js → index-BRLJm4Du.js} +1 -1
- langflow/frontend/assets/{index-B1pCwO7w.js → index-BSZfrTzR.js} +1 -1
- langflow/frontend/assets/{index-BC1IeVU1.js → index-BTPSO7Ty.js} +1 -1
- langflow/frontend/assets/{index-b-rN9AfQ.js → index-BUepyNwR.js} +1 -1
- langflow/frontend/assets/{index-Btpd5ABe.js → index-Bhm1dylP.js} +1 -1
- langflow/frontend/assets/{index-D2VizGDj.js → index-BjcscQOG.js} +1 -1
- langflow/frontend/assets/{index-ClhZDgl7.js → index-Bki1gbZ4.js} +1 -1
- langflow/frontend/assets/{index-C9k4JNN9.js → index-BlSZbUqf.js} +1 -1
- langflow/frontend/assets/{index-D8JX1cRS.js → index-BnRJS1jE.js} +1 -1
- langflow/frontend/assets/{index-D0pxkcHO.js → index-BorUWqkM.js} +1 -1
- langflow/frontend/assets/{index-B9HjUWVE.js → index-Bpv14pFu.js} +1 -1
- langflow/frontend/assets/{index-Bwsn-lQB.js → index-Br2kJx5F.js} +1 -1
- langflow/frontend/assets/{index-DesgBINf.js → index-BsVHbutF.js} +1 -1
- langflow/frontend/assets/{index-BBq18hqx.js → index-BtXZCxsi.js} +1 -1
- langflow/frontend/assets/{index-DBpxcoMV.js → index-Buf0FLlA.js} +1 -1
- langflow/frontend/assets/{index-D209-7aw.js → index-BwWOFcx9.js} +1 -1
- langflow/frontend/assets/{index-CrBs1nwc.js → index-BxPGc9G_.js} +1 -1
- langflow/frontend/assets/{index-C1f4etee.js → index-Bxa7Zqaq.js} +1 -1
- langflow/frontend/assets/{index-DIV6505q.js → index-BymhxBmN.js} +1 -1
- langflow/frontend/assets/{index-C-r14tFz.js → index-C2fMifvM.js} +1 -1
- langflow/frontend/assets/{index-DQ1FRhsz.js → index-C3Bj4_FP.js} +1 -1
- langflow/frontend/assets/{index-u8xPJhQ9.js → index-C9KrGJ67.js} +1 -1
- langflow/frontend/assets/{index-BLWD3_vy.js → index-C9ZDIZFW.js} +1 -1
- langflow/frontend/assets/{index-s4nJc-Yr.js → index-C9dL6qYU.js} +1 -1
- langflow/frontend/assets/{index-Cygo70hO.js → index-CBGa-ML1.js} +1 -1
- langflow/frontend/assets/{index-YcrG1VlA.js → index-CBOYVi1U.js} +1 -1
- langflow/frontend/assets/{index-DOZbHiHX.js → index-CDDBjrKl.js} +1 -1
- langflow/frontend/assets/{index-COKDBl8e.js → index-CDN9FxX3.js} +1 -1
- langflow/frontend/assets/{index-B7_RAtW2.js → index-CEau7eX7.js} +1 -1
- langflow/frontend/assets/{index-D6He5wXg.js → index-CJ5EUvrn.js} +1 -1
- langflow/frontend/assets/{index-gQhP4vXD.js → index-CSe6UjxV.js} +1 -1
- langflow/frontend/assets/{index-B0Ln4Y0M.js → index-CTIjE6ec.js} +1 -1
- langflow/frontend/assets/{index-CuYwSn3U.js → index-C_H9PvPC.js} +1 -1
- langflow/frontend/assets/{index-BC3CIG8d.js → index-CbN90tiv.js} +1 -1
- langflow/frontend/assets/{index-VyN_s8K0.js → index-CcS86fAH.js} +1 -1
- langflow/frontend/assets/{index-Bx2IXtdQ.js → index-CdPp99Ov.js} +1 -1
- langflow/frontend/assets/{index-CjgKQmu-.js → index-CdaKd40h.js} +1 -1
- langflow/frontend/assets/{index-BcataUkS.js → index-ChPNbgSb.js} +1 -1
- langflow/frontend/assets/{index-B_0PBktA.js → index-ChyV4bY1.js} +1 -1
- langflow/frontend/assets/{index-B2q8-22d.js → index-CmPIFu12.js} +1 -1
- langflow/frontend/assets/{index-C5itu9AQ.js → index-CmSCL35l.js} +1 -1
- langflow/frontend/assets/{index-CaAxZTAQ.js → index-CoAX4KHA.js} +1 -1
- langflow/frontend/assets/{index-_9h3RxcL.js → index-CogD177T.js} +1 -1
- langflow/frontend/assets/{index-C1roq-7s.js → index-Cqc93Aww.js} +1 -1
- langflow/frontend/assets/{index-rjWOnQKQ.js → index-CtzhOjCJ.js} +1 -1
- langflow/frontend/assets/{index-CpwmD_9A.js → index-D2K8Dxca.js} +1 -1
- langflow/frontend/assets/{index-DSicyBaT.js → index-D38ii1eC.js} +1 -1
- langflow/frontend/assets/{index-DPevQFZ6.js → index-D6iJ0ixS.js} +1 -1
- langflow/frontend/assets/{index-BQVlWbJq.js → index-D6qchtKo.js} +1 -1
- langflow/frontend/assets/{index-D1j7yWdb.js → index-D8nmnanz.js} +1 -1
- langflow/frontend/assets/{index-D62Ch4Dv.js → index-DCNa4h3y.js} +1 -1
- langflow/frontend/assets/{index-CVV9YFq4.js → index-DEwfBIGT.js} +1 -1
- langflow/frontend/assets/{index-C9ZZ6URM.js → index-DFckK5jC.js} +1 -1
- langflow/frontend/assets/{index-DGL66TBK.js → index-DGV6oDTb.js} +1 -1
- langflow/frontend/assets/{index-B3iG0ucj.js → index-DJzLRh66.js} +1 -1
- langflow/frontend/assets/{index-DiYFoC0a.js → index-DKho7fub.js} +1 -1
- langflow/frontend/assets/{index-CMEkIANd.js → index-DLoNvjqt.js} +1 -1
- langflow/frontend/assets/{index-BqGGFyiq.js → index-DNOP-INQ.js} +1 -1
- langflow/frontend/assets/{index-D0GIiSAn.js → index-DOZ4j-co.js} +1 -1
- langflow/frontend/assets/{index-dHc2SZAk.js → index-DXMYOwee.js} +1 -1
- langflow/frontend/assets/{index-zoFDYFyk.js → index-DYj_t4sx.js} +1 -1
- langflow/frontend/assets/{index-Ca-iIvgT.js → index-DYwMDNLL.js} +1 -1
- langflow/frontend/assets/{index-ue1deu4E.js → index-DZ4RRuo1.js} +1 -1
- langflow/frontend/assets/{index-0ttxvdXm.js → index-D_AWHRfe.js} +1 -1
- langflow/frontend/assets/{index-EQ9XVZMK.js → index-DaosAMPL.js} +1 -1
- langflow/frontend/assets/{index-B6T6iwzz.js → index-DcTv_MVV.js} +1 -1
- langflow/frontend/assets/{index-CHvEDMhh.js → index-DeMmL8Mo.js} +1 -1
- langflow/frontend/assets/{index-Jurj0j25.js → index-Dgd8GGgG.js} +1 -1
- langflow/frontend/assets/{index-B7g04R_m.js → index-DgghztJk.js} +1 -1
- langflow/frontend/assets/{index-DFI7sw8T.js → index-Dgtl-Ryc.js} +1 -1
- langflow/frontend/assets/{index-ByJ8GzPq.js → index-DiYhK7Hs.js} +1 -1
- langflow/frontend/assets/{index-Dc9kV0Ka.js → index-Dj6hJobz.js} +1 -1
- langflow/frontend/assets/{index-DRKtgHt5.js → index-DjNRiQay.js} +1 -1
- langflow/frontend/assets/{index-C9Th6E-p.js → index-DmZZmu5_.js} +1 -1
- langflow/frontend/assets/{index--M5OJlKX.js → index-DpMb4SyP.js} +1 -1
- langflow/frontend/assets/{index-BcKmzZOu.js → index-DtjOwLQC.js} +1 -1
- langflow/frontend/assets/{index-BYKbvQCV.js → index-DufevlPT.js} +1 -1
- langflow/frontend/assets/{index-CXHt8rUv.js → index-DvZ2dP1K.js} +1 -1
- langflow/frontend/assets/{index-CAmjfJVt.js → index-DyIpqQ6u.js} +1 -1
- langflow/frontend/assets/{index-BLoFUaRi.js → index-DysqOh2v.js} +1 -1
- langflow/frontend/assets/{index-K9fveZF2.js → index-DzFPLySG.js} +1 -1
- langflow/frontend/assets/{index-Dw4kgUUj.js → index-GF2LJgkR.js} +1 -1
- langflow/frontend/assets/{index-BzfMIVWu.js → index-GUM5ctOD.js} +1 -1
- langflow/frontend/assets/{index-BkXXR4oe.js → index-H15pIDqA.js} +1 -1
- langflow/frontend/assets/{index-CIH7XBmr.js → index-HbhXQ4iI.js} +1 -1
- langflow/frontend/assets/{index-Ct7GuQjS.js → index-KcUnsxlf.js} +1 -1
- langflow/frontend/assets/{index-DjH3O0u7.js → index-N3NiikE-.js} +1 -1
- langflow/frontend/assets/{index-DM3wOq2U.js → index-O3c1ky7v.js} +1 -1
- langflow/frontend/assets/{index-OpC5rDNC.js → index-Q0Ex46_F.js} +1 -1
- langflow/frontend/assets/{index-CQnnGuA4.js → index-TUSpXwVe.js} +1 -1
- langflow/frontend/assets/{index-B4K5BWeu.js → index-UVKuDcRW.js} +1 -1
- langflow/frontend/assets/{index-Cw1n0VgG.js → index-Vx9vra5E.js} +1 -1
- langflow/frontend/assets/{index-DGrvGoqZ.js → index-YXNg_MqK.js} +1 -1
- langflow/frontend/assets/{index-rUaDK5fV.js → index-Yrff3uIT.js} +1 -1
- langflow/frontend/assets/{index-W4Xgxc86.js → index-ZHQE-way.js} +1 -1
- langflow/frontend/assets/{index-D7-oveQq.js → index-ZPrp_qyL.js} +1 -1
- langflow/frontend/assets/{index-BRtNHpy5.js → index-_AhGSFEp.js} +1 -1
- langflow/frontend/assets/{index-fieuePG_.js → index-cOLi_68F.js} +1 -1
- langflow/frontend/assets/{index-Cn8OX_CM.js → index-dg4h4aV9.js} +1 -1
- langflow/frontend/assets/{index-BSNb6jUq.js → index-fU437Xuh.js} +1 -1
- langflow/frontend/assets/{index-CHc4cSsn.js → index-gnvplQfi.js} +1 -1
- langflow/frontend/assets/{index-BzA_2OOX.js → index-gsz-bhOF.js} +1 -1
- langflow/frontend/assets/{index-_isovt-w.js → index-k9UFhzxg.js} +1 -1
- langflow/frontend/assets/{index-ClWYF093.js → index-kjgyrJKk.js} +1 -1
- langflow/frontend/assets/{index-DeC3GJP9.js → index-lJ1V4apH.js} +1 -1
- langflow/frontend/assets/{index-BFk7GUrj.js → index-lp0TR1VG.js} +1 -1
- langflow/frontend/assets/{index-DwRpKQY3.js → index-p77FDSRr.js} +1 -1
- langflow/frontend/assets/{index-Bd1TQW2N.js → index-pwj4p8sb.js} +1 -1
- langflow/frontend/assets/{index-e7U99ghU.js → index-uxytSC6u.js} +1 -1
- langflow/frontend/assets/lazyIconImports-B2rERpGa.js +2 -0
- langflow/frontend/assets/{use-post-add-user-Cv--9zv4.js → use-post-add-user-CNxc7cHz.js} +1 -1
- langflow/frontend/index.html +1 -1
- langflow/initial_setup/starter_projects/Basic Prompt Chaining.json +8 -2
- langflow/initial_setup/starter_projects/Basic Prompting.json +12 -3
- langflow/initial_setup/starter_projects/Blog Writer.json +20 -5
- langflow/initial_setup/starter_projects/Custom Component Generator.json +16 -4
- langflow/initial_setup/starter_projects/Document Q&A.json +16 -4
- langflow/initial_setup/starter_projects/Financial Report Parser.json +12 -3
- langflow/initial_setup/starter_projects/Hybrid Search RAG.json +24 -6
- langflow/initial_setup/starter_projects/Image Sentiment Analysis.json +16 -4
- langflow/initial_setup/starter_projects/Instagram Copywriter.json +28 -7
- langflow/initial_setup/starter_projects/Invoice Summarizer.json +16 -4
- langflow/initial_setup/starter_projects/Market Research.json +16 -4
- langflow/initial_setup/starter_projects/Meeting Summary.json +36 -9
- langflow/initial_setup/starter_projects/Memory Chatbot.json +16 -4
- langflow/initial_setup/starter_projects/News Aggregator.json +16 -4
- langflow/initial_setup/starter_projects/Nvidia Remix.json +20 -5
- langflow/initial_setup/starter_projects/Pok/303/251dex Agent.json" +12 -3
- langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json +12 -3
- langflow/initial_setup/starter_projects/Price Deal Finder.json +16 -4
- langflow/initial_setup/starter_projects/Research Agent.json +28 -7
- langflow/initial_setup/starter_projects/Research Translation Loop.json +24 -6
- langflow/initial_setup/starter_projects/SEO Keyword Generator.json +12 -3
- langflow/initial_setup/starter_projects/SaaS Pricing.json +12 -3
- langflow/initial_setup/starter_projects/Search agent.json +12 -3
- langflow/initial_setup/starter_projects/Sequential Tasks Agents.json +32 -8
- langflow/initial_setup/starter_projects/Simple Agent.json +16 -4
- langflow/initial_setup/starter_projects/Social Media Agent.json +16 -4
- langflow/initial_setup/starter_projects/Text Sentiment Analysis.json +12 -3
- langflow/initial_setup/starter_projects/Travel Planning Agents.json +16 -4
- langflow/initial_setup/starter_projects/Twitter Thread Generator.json +36 -9
- langflow/initial_setup/starter_projects/Vector Store RAG.json +33 -9
- langflow/initial_setup/starter_projects/Youtube Analysis.json +20 -5
- langflow/interface/components.py +32 -16
- {langflow_base_nightly-0.5.0.dev9.dist-info → langflow_base_nightly-0.5.0.dev11.dist-info}/METADATA +1 -1
- {langflow_base_nightly-0.5.0.dev9.dist-info → langflow_base_nightly-0.5.0.dev11.dist-info}/RECORD +165 -164
- langflow/frontend/assets/lazyIconImports-DRAvJxst.js +0 -2
- {langflow_base_nightly-0.5.0.dev9.dist-info → langflow_base_nightly-0.5.0.dev11.dist-info}/WHEEL +0 -0
- {langflow_base_nightly-0.5.0.dev9.dist-info → langflow_base_nightly-0.5.0.dev11.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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
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
|
|
98
|
-
resources
|
|
99
|
-
|
|
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
|
|
140
|
-
"""Handle resource read requests."""
|
|
141
|
-
|
|
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
|
|
178
|
-
tools
|
|
179
|
-
|
|
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
|
|
225
|
-
"""Handle tool execution requests."""
|
|
226
|
-
|
|
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/")
|
langflow/api/v1/mcp_projects.py
CHANGED
|
@@ -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
|
|
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.
|
|
27
|
-
from langflow.api.v1.mcp import (
|
|
24
|
+
from langflow.api.v1.mcp_utils import (
|
|
28
25
|
current_user_ctx,
|
|
29
|
-
|
|
26
|
+
handle_call_tool,
|
|
27
|
+
handle_list_resources,
|
|
28
|
+
handle_list_tools,
|
|
30
29
|
handle_mcp_errors,
|
|
31
|
-
|
|
30
|
+
handle_read_resource,
|
|
32
31
|
)
|
|
33
|
-
from langflow.api.v1.schemas import MCPInstallRequest, MCPSettings
|
|
34
|
-
from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH
|
|
35
|
-
from langflow.base.mcp.util import
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
698
|
-
resources
|
|
699
|
-
|
|
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
|
|
738
|
-
"""Handle resource read requests."""
|
|
739
|
-
|
|
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
|
|
776
|
-
"""Handle tool execution requests."""
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|