"""OpenTelemetry parent spans for each MCP tool invocation (nests httpx child spans).""" from __future__ import annotations import functools import inspect from collections.abc import Callable from typing import Any, cast from opentelemetry import trace _tracer = trace.get_tracer("data360.mcp.tools") def instrument_mcp_tool(fn: Callable[..., Any], *, tool_name: str) -> Callable[..., Any]: """Wrap a tool function so each call runs under ``mcp.tool.`` span.""" if inspect.iscoroutinefunction(fn): fn_async = cast("Callable[..., Any]", fn) @functools.wraps(fn_async) async def _async_impl(*args: Any, **kwargs: Any) -> Any: with _tracer.start_as_current_span( f"mcp.tool.{tool_name}", attributes={"mcp.tool.name": tool_name}, ): return await fn_async(*args, **kwargs) return _async_impl fn_sync = cast("Callable[..., Any]", fn) @functools.wraps(fn_sync) def _sync_impl(*args: Any, **kwargs: Any) -> Any: with _tracer.start_as_current_span( f"mcp.tool.{tool_name}", attributes={"mcp.tool.name": tool_name}, ): return fn_sync(*args, **kwargs) return _sync_impl