Skip to content

MCP Instrumentation

Model Context Protocol (MCP) is an open standard for connecting LLM agents to external tools and data sources. TruLens provides first-class support for instrumenting MCP tool calls via the MCP span type and a dedicated set of semantic attributes.

MCP span attributes

When you instrument an MCP tool call, TruLens captures the following attributes in the ai.observability.mcp.* namespace:

Attribute Description Type
ai.observability.mcp.tool_name Name of the MCP tool being called str
ai.observability.mcp.tool_description Description of the MCP tool str
ai.observability.mcp.server_name Name of the MCP server providing the tool str
ai.observability.mcp.input_schema JSON schema of the tool's input parameters str
ai.observability.mcp.input_arguments Arguments passed to the tool str
ai.observability.mcp.output_content Content returned by the tool str
ai.observability.mcp.output_is_error Whether the tool call resulted in an error bool
ai.observability.mcp.execution_time_ms Time taken to execute the tool call (ms) float

Instrumenting MCP tool calls

Use @instrument with span_type=SpanAttributes.SpanType.MCP to mark a method as an MCP tool call. Map the relevant attributes using the SpanAttributes.MCP constants:

from trulens.core.otel.instrument import instrument
from trulens.otel.semconv.trace import SpanAttributes


class MCPClient:
    @instrument(
        span_type=SpanAttributes.SpanType.MCP,
        attributes={
            SpanAttributes.MCP.TOOL_NAME: "tool_name",
            SpanAttributes.MCP.INPUT_ARGUMENTS: "arguments",
            SpanAttributes.MCP.OUTPUT_CONTENT: "return",
        },
    )
    def call_tool(self, tool_name: str, arguments: dict) -> str:
        """Call an MCP tool and return its output."""
        ...

Full working example

The example below shows a complete setup: an MCP client that calls a weather tool, wrapped with TruLens instrumentation and evaluated for tool call quality.

import json
from trulens.core import TruSession, Metric, Selector
from trulens.apps.app import TruApp
from trulens.core.otel.instrument import instrument
from trulens.otel.semconv.trace import SpanAttributes
from trulens.providers.openai import OpenAI

# --- App definition ---

class WeatherMCPClient:
    """Simulates an MCP client that calls a weather tool."""

    TOOLS = {
        "get_weather": {
            "description": "Get current weather for a city",
            "input_schema": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "units": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["city"],
            },
        }
    }

    @instrument(
        span_type=SpanAttributes.SpanType.MCP,
        attributes={
            SpanAttributes.MCP.TOOL_NAME: "tool_name",
            SpanAttributes.MCP.TOOL_DESCRIPTION: lambda ret, exc, *args, **kwargs: (
                WeatherMCPClient.TOOLS.get(kwargs.get("tool_name", ""), {}).get(
                    "description", ""
                )
            ),
            SpanAttributes.MCP.INPUT_ARGUMENTS: lambda ret, exc, *args, **kwargs: (
                json.dumps(kwargs.get("arguments", {}))
            ),
            SpanAttributes.MCP.OUTPUT_CONTENT: "return",
            SpanAttributes.MCP.OUTPUT_IS_ERROR: lambda ret, exc, *args, **kwargs: (
                exc is not None
            ),
        },
    )
    def call_tool(self, tool_name: str, arguments: dict) -> str:
        """Call an MCP tool."""
        if tool_name == "get_weather":
            city = arguments.get("city", "Unknown")
            units = arguments.get("units", "celsius")
            # Simulated tool response
            return json.dumps({
                "city": city,
                "temperature": 22 if units == "celsius" else 72,
                "units": units,
                "condition": "Partly cloudy",
            })
        raise ValueError(f"Unknown tool: {tool_name}")

    @instrument(
        span_type=SpanAttributes.SpanType.RECORD_ROOT,
        attributes={
            SpanAttributes.RECORD_ROOT.INPUT: "query",
            SpanAttributes.RECORD_ROOT.OUTPUT: "return",
        },
    )
    def answer(self, query: str) -> str:
        """Answer a user query by calling MCP tools."""
        result = self.call_tool("get_weather", {"city": "San Francisco", "units": "celsius"})
        data = json.loads(result)
        return (
            f"The weather in {data['city']} is {data['temperature']}Β°C "
            f"and {data['condition']}."
        )


# --- TruLens setup ---

session = TruSession()
provider = OpenAI()

metrics = [
    Metric(
        implementation=provider.tool_selection_with_cot_reasons,
        name="Tool Selection",
        selectors={"trace": Selector(trace_level=True)},
    ),
    Metric(
        implementation=provider.tool_calling_with_cot_reasons,
        name="Tool Calling",
        selectors={"trace": Selector(trace_level=True)},
    ),
    Metric(
        implementation=provider.tool_quality_with_cot_reasons,
        name="Tool Quality",
        selectors={"trace": Selector(trace_level=True)},
    ),
]

mcp_client = WeatherMCPClient()

tru_app = TruApp(
    mcp_client,
    app_name="WeatherMCPClient",
    app_version="v1",
    metrics=metrics,
)

# --- Run and evaluate ---

with tru_app as recording:
    response = mcp_client.answer("What's the weather in San Francisco?")

print(response)

session.get_leaderboard()

Capturing execution time with lambda attributes

If your MCP client reports latency separately, you can capture it using a lambda:

import time

class TimedMCPClient:
    @instrument(
        span_type=SpanAttributes.SpanType.MCP,
        attributes=lambda ret, exc, *args, **kwargs: {
            SpanAttributes.MCP.TOOL_NAME: kwargs.get("tool_name"),
            SpanAttributes.MCP.OUTPUT_CONTENT: ret,
            SpanAttributes.MCP.OUTPUT_IS_ERROR: exc is not None,
            # execution_time_ms captured from the tool response metadata
            SpanAttributes.MCP.EXECUTION_TIME_MS: (
                ret.get("latency_ms") if isinstance(ret, dict) else None
            ),
        },
    )
    def call_tool(self, tool_name: str, arguments: dict):
        ...

Evaluating MCP tool calls

MCP spans are compatible with the agentic evaluators. To evaluate tool selection and calling quality across MCP spans, use the Metric API with trace-level selectors:

from trulens.core import Metric, Selector
from trulens.providers.openai import OpenAI

provider = OpenAI()

metrics = [
    Metric(
        implementation=provider.tool_selection_with_cot_reasons,
        name="Tool Selection",
        selectors={"trace": Selector(trace_level=True)},
    ),
    Metric(
        implementation=provider.tool_calling_with_cot_reasons,
        name="Tool Calling",
        selectors={"trace": Selector(trace_level=True)},
    ),
    Metric(
        implementation=provider.tool_quality_with_cot_reasons,
        name="Tool Quality",
        selectors={"trace": Selector(trace_level=True)},
    ),
]

See the Metric migration guide for the full API.