mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 03:30:34 +00:00
* fix(mcp): keep a tool argument named runtime out of LangGraph's runtime injection The pooled stdio wrapper and the background-submit wrapper took LangGraph's ToolRuntime in a parameter called `runtime`. ToolNode injects by that name, so a tool whose own input schema has a `runtime` property had the model's value replaced by the ToolRuntime and then lost: the server received the other arguments only, and TaskSubmitRequest.arguments missed it too. Name the injected parameter `deerflow_runtime` in both wrappers (it is still detected through its ToolRuntime annotation) and read it under that name in the personal-MCP ownership guard. Existing tests that call the wrappers pass the new name; two ToolNode tests cover a `runtime` business argument on each path. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(mcp): read the injected runtime under the right name in the personal-tool guard The personal-tool ownership guard read kwargs["deerflow_runtime"], but only the pooled stdio wrapper was renamed; personal HTTP/SSE tools still carry langchain-mcp-adapters' `runtime` parameter, so the guard never saw the authoritative runtime user for them. Look the parameter name up per tool, and update the mcp AGENTS.md note on how the runtime is injected. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
78 lines
3.0 KiB
Python
78 lines
3.0 KiB
Python
"""Load only the caller's personal tools; never publish them in the global cache."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import functools
|
|
import inspect
|
|
import threading
|
|
|
|
from langchain_core.tools import ToolException
|
|
|
|
from deerflow.mcp.personal_access import require_personal_mcp_access
|
|
from deerflow.mcp.user_config import PersonalMcpConfigSnapshot, load_user_mcp_config, load_user_mcp_config_if_changed
|
|
from deerflow.runtime.user_context import resolve_runtime_user_id
|
|
from deerflow.tools.mcp_metadata import get_mcp_source
|
|
from deerflow.tools.sync import make_sync_tool_wrapper
|
|
|
|
|
|
def _runtime_parameter(coroutine) -> str:
|
|
"""Name of the parameter LangGraph injects the ``ToolRuntime`` into.
|
|
|
|
Pooled stdio wrappers expose ``deerflow_runtime`` so a business argument
|
|
named ``runtime`` survives; HTTP/SSE tools keep ``langchain-mcp-adapters``'
|
|
own ``runtime`` parameter.
|
|
"""
|
|
try:
|
|
parameters = inspect.signature(coroutine).parameters
|
|
except (TypeError, ValueError):
|
|
return "runtime"
|
|
return "deerflow_runtime" if "deerflow_runtime" in parameters else "runtime"
|
|
|
|
|
|
def _guard(tool, owner: str, server_name: str):
|
|
original = tool.coroutine
|
|
runtime_parameter = _runtime_parameter(original)
|
|
snapshot: PersonalMcpConfigSnapshot | None = None
|
|
snapshot_lock = threading.Lock()
|
|
|
|
def current_connection():
|
|
nonlocal snapshot
|
|
with snapshot_lock:
|
|
snapshot = load_user_mcp_config_if_changed(owner, snapshot)
|
|
server = snapshot.config.mcp_servers.get(server_name)
|
|
return server if server is not None and server.enabled else None
|
|
|
|
@functools.wraps(original)
|
|
async def invoke(*args, **kwargs):
|
|
if resolve_runtime_user_id(kwargs.get(runtime_parameter)) != owner:
|
|
raise ToolException("This MCP connection belongs to another user")
|
|
server = await asyncio.to_thread(current_connection)
|
|
if server is None:
|
|
raise ToolException("This personal MCP connection was changed, disabled or removed; start a new run")
|
|
await require_personal_mcp_access(owner, server)
|
|
return await original(*args, **kwargs)
|
|
|
|
return tool.model_copy(update={"coroutine": invoke, "func": make_sync_tool_wrapper(invoke, tool.name)})
|
|
|
|
|
|
async def _load(owner: str, config):
|
|
from deerflow.mcp.tools import get_mcp_tools
|
|
|
|
tools = await get_mcp_tools(config, personal_user_id=owner)
|
|
return [_guard(tool, owner, source["server_name"]) for tool in tools if (source := get_mcp_source(tool))]
|
|
|
|
|
|
def get_user_mcp_tools():
|
|
"""Discover from one literal snapshot, with an ownership check on every call.
|
|
|
|
Deliberately keep personal discovery out of the deployment's singleton
|
|
cache. Stdio session reuse remains owner/thread/revision-scoped.
|
|
"""
|
|
owner = resolve_runtime_user_id(None)
|
|
config = load_user_mcp_config(owner)
|
|
if not config.get_enabled_mcp_servers():
|
|
return [], config
|
|
tools = make_sync_tool_wrapper(_load, "personal MCP discovery")(owner, config)
|
|
return tools, config
|