Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions py/nox_session_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"test_langchain": {"max_python_exclusive": "3.15"}, # ormsgpack
"test_openrouter": {"versions": {"latest": {"max_python_exclusive": "3.15"}}}, # pydantic-core 2.41
"test_openai_ddtrace": {"max_python_exclusive": "3.15"}, # ddtrace requires Python < 3.15
# fastmcp 2.x depends on lupa, which has no cp315 wheels and does not build on Windows.
"test_fastmcp": {"versions": {"not_latest_below": {"version": "3.0.0", "max_python_exclusive": "3.15"}}},
}


Expand Down
28 changes: 28 additions & 0 deletions py/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ def _pinned_python_version():
OPENAI_VERSIONS = _get_matrix_versions("openai")
OPENAI_ENDPOINT_VERSIONS = (OPENAI_VERSIONS[0], OPENAI_VERSIONS[-1])

MCP_VERSIONS = _get_matrix_versions("mcp")
FASTMCP_VERSIONS = _get_matrix_versions("fastmcp")
FASTMCP_SLIM_VERSIONS = _get_matrix_versions("fastmcp-slim")


def _register_matrix_sessions(specs):
"""Register the routine provider sessions from compact data records."""
Expand Down Expand Up @@ -331,6 +335,30 @@ def run(session, version, spec=spec):
"dependencies": [("instructor", "$version")],
"tests": f"{INTEGRATION_DIR}/instructor/test_instructor.py",
},
{
"name": "test_mcp",
"doc": "MCP server instrumentation on the bundled FastMCP (1.x) / MCPServer (2.x).",
"versions": MCP_VERSIONS,
"dependencies": [("mcp", "$version")],
"tests": f"{INTEGRATION_DIR}/mcp/test_mcp.py",
"env": {"BRAINTRUST_MCP_TEST_FLAVOR": "mcp"},
},
{
"name": "test_fastmcp",
"doc": "MCP server instrumentation on standalone FastMCP.",
"versions": FASTMCP_VERSIONS,
"dependencies": [("fastmcp", "$version")],
"tests": f"{INTEGRATION_DIR}/mcp/test_mcp.py",
"env": {"BRAINTRUST_MCP_TEST_FLAVOR": "fastmcp"},
},
{
"name": "test_fastmcp_slim",
"doc": "MCP server instrumentation on fastmcp-slim without the fastmcp meta-package.",
"versions": FASTMCP_SLIM_VERSIONS,
"dependencies": [("fastmcp-slim", "$version")],
"tests": f"{INTEGRATION_DIR}/mcp/test_mcp.py",
"env": {"BRAINTRUST_MCP_TEST_FLAVOR": "fastmcp"},
},
{
"name": "test_openai",
"versions": OPENAI_VERSIONS,
Expand Down
21 changes: 21 additions & 0 deletions py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,25 @@ latest = "livekit-agents==1.8.9"
latest = "pipecat-ai==1.14.0"
"1.3.0" = "pipecat-ai==1.3.0"

[tool.braintrust.matrix.mcp]
latest = "mcp==2.3.0"
"2.0.0" = "mcp==2.0.0"
"1.30.0" = "mcp==1.30.0"
"1.28.1" = "mcp==1.28.1"

# Standalone FastMCP. 2.x and 3.x run on mcp 1.x; 4.x runs on mcp 2.x. Since
# 3.4 the implementation ships in fastmcp-slim and fastmcp only pins it.
[tool.braintrust.matrix.fastmcp]
latest = "fastmcp==4.1.3"
"4.0.0" = "fastmcp==4.0.0"
"3.4.11" = "fastmcp==3.4.11"
"3.0.0" = "fastmcp==3.0.0"
"2.14.7" = "fastmcp==2.14.7"

[tool.braintrust.matrix.fastmcp-slim]
latest = "fastmcp-slim[client,server]==4.1.3"
"3.4.11" = "fastmcp-slim[client,server]==3.4.11"

[tool.braintrust.matrix.claude-agent-sdk]
latest = "claude-agent-sdk==0.2.174"
# The integration still supports >=0.1.10, but 0.1.9-0.1.17 were deleted from
Expand Down Expand Up @@ -713,6 +732,8 @@ instructor = "instructor"
litellm = "litellm"
livekit-agents = "livekit.agents"
mistralai = "mistralai"
mcp = "mcp"
fastmcp = "fastmcp"
huggingface-hub = "huggingface_hub"
openai = "openai"
openai-agents = "agents"
Expand Down
3 changes: 3 additions & 0 deletions py/src/braintrust/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"LiteLLMIntegration": "litellm",
"LiveKitAgentsIntegration": "livekit_agents",
"LlamaIndexIntegration": "llamaindex",
"MCPIntegration": "mcp",
"MistralIntegration": "mistral",
"OpenAIIntegration": "openai",
"OpenAIAgentsIntegration": "openai_agents",
Expand Down Expand Up @@ -59,6 +60,7 @@
from .litellm import LiteLLMIntegration as LiteLLMIntegration
from .livekit_agents import LiveKitAgentsIntegration as LiveKitAgentsIntegration
from .llamaindex import LlamaIndexIntegration as LlamaIndexIntegration
from .mcp import MCPIntegration as MCPIntegration
from .mistral import MistralIntegration as MistralIntegration
from .openai import OpenAIIntegration as OpenAIIntegration
from .openai_agents import OpenAIAgentsIntegration as OpenAIAgentsIntegration
Expand Down Expand Up @@ -92,6 +94,7 @@
"LiveKitAgentsIntegration",
"LangChainIntegration",
"LlamaIndexIntegration",
"MCPIntegration",
"MistralIntegration",
"OpenAIIntegration",
"OpenAIAgentsIntegration",
Expand Down
62 changes: 62 additions & 0 deletions py/src/braintrust/integrations/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Braintrust instrumentation for Model Context Protocol (MCP) servers."""

import logging
from typing import Any, TypeVar

from braintrust.integrations.versioning import version_satisfies

from .integration import MCPIntegration
from .patchers import ServerRunPatcher
from .tracing import SERVER_CONFIG_ATTR, MCPServerConfig


logger = logging.getLogger(__name__)

ServerT = TypeVar("ServerT")


def _resolve_lowlevel_server(server: Any) -> Any | None:
try:
from mcp.server.lowlevel.server import Server
except ImportError:
return None
if isinstance(server, Server):
return server
# Bundled FastMCP (1.x), MCPServer (2.x), and standalone fastmcp.FastMCP
# all delegate protocol handling to a low-level server.
for attr in ("_lowlevel_server", "_mcp_server"):
candidate = getattr(server, attr, None)
if isinstance(candidate, Server):
return candidate
return None


def wrap_mcp_server(server: ServerT, capture_content: bool = True) -> ServerT:
"""Trace incoming MCP requests to *server*, from receipt through response sending.

Accepts a low-level ``mcp.server.lowlevel.Server``, the bundled
``FastMCP`` (MCP 1.x) or ``MCPServer`` (MCP 2.x), or a standalone
``fastmcp.FastMCP`` server, and returns the same object. Calling it again
updates the settings without adding duplicate spans.

:param server: The MCP server to trace.
:param capture_content: Log tool and prompt arguments, results, resource
contents, and notification parameters (default). When False, spans keep
names, timing, allowlisted metadata, and error classification only.
"""
lowlevel = _resolve_lowlevel_server(server)
if lowlevel is None:
logger.warning("Unsupported MCP server %r. Not wrapping.", type(server).__name__)
return server
if not MCPIntegration.install_patchers():
logger.warning("Unsupported mcp version. Not wrapping.")
return server
setattr(lowlevel, SERVER_CONFIG_ATTR, MCPServerConfig(capture_content=capture_content))
import mcp

if version_satisfies(MCPIntegration.detect_version(mcp), ServerRunPatcher.version_spec):
ServerRunPatcher.wrap_target(lowlevel)
return server


__all__ = ["MCPIntegration", "wrap_mcp_server"]
Loading
Loading