Aari 47b258ebd7
feat(mcp): add ordinary durable task driver (#4690)
* feat(mcp): add durable task runtime foundation

* fix(chart): sync embedded config version

* fix(mcp): isolate task polls during shutdown

* feat(mcp): track consecutive poll errors on mcp_tasks

poll_attempt_count grows on every claim (successful polls included), so it
cannot drive a failure backoff without misjudging normal long tasks. Add
consecutive_poll_error_count: incremented when a claim is released after a
poll error, reset to zero by any applied snapshot. The backoff/terminal
policy that consumes it lands with the first concrete driver.

* fix(mcp): harden durable task lifecycle

* feat(mcp): add ordinary durable task driver

* test(mcp): address durable task review feedback

* fix(mcp): preserve submit tool descriptions

* fix(mcp): bound remote task calls

* fix(mcp): bound persisted task payloads

* fix(mcp): preserve task tool error details

* fix(mcp): enforce durable task boundaries

* test(mcp): cover task config snapshot lifecycle

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-15 14:26:38 +08:00

58 lines
2.0 KiB
Python

"""Shared construction of MCP tool-call interceptors."""
from __future__ import annotations
import logging
from typing import Any
from deerflow.config.extensions_config import ExtensionsConfig
from deerflow.mcp.oauth import build_oauth_tool_interceptor
from deerflow.reflection import resolve_variable
logger = logging.getLogger(__name__)
def build_mcp_tool_interceptors(
extensions_config: ExtensionsConfig,
*,
oauth_builder: Any = build_oauth_tool_interceptor,
resolver: Any = resolve_variable,
target_logger: logging.Logger = logger,
) -> list[Any]:
"""Build OAuth followed by configured custom MCP interceptors."""
interceptors: list[Any] = []
oauth_interceptor = oauth_builder(extensions_config)
if oauth_interceptor is not None:
interceptors.append(oauth_interceptor)
raw_paths = (extensions_config.model_extra or {}).get("mcpInterceptors")
if isinstance(raw_paths, str):
raw_paths = [raw_paths]
elif not isinstance(raw_paths, list):
if raw_paths is not None:
target_logger.warning(
"mcpInterceptors must be a list of strings, got %s; skipping",
type(raw_paths).__name__,
)
raw_paths = []
for interceptor_path in raw_paths:
try:
builder = resolver(interceptor_path)
interceptor = builder()
if callable(interceptor):
interceptors.append(interceptor)
target_logger.info("Loaded MCP interceptor: %s", interceptor_path)
elif interceptor is not None:
target_logger.warning(
"Builder %s returned non-callable %s; skipping",
interceptor_path,
type(interceptor).__name__,
)
except Exception:
target_logger.warning(
f"Failed to load MCP interceptor {interceptor_path}",
exc_info=True,
)
return interceptors