Skip to content

Commit 9bddd09

Browse files
MCP Streamable HTTP Spans (DO NOT MERGE YET) (#1462)
* Add tool manager span. * Add tool manager spans to MCP instrumentation to support streaming. --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 9420b06 commit 9bddd09

File tree

4 files changed

+47
-3
lines changed

4 files changed

+47
-3
lines changed

newrelic/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2851,6 +2851,8 @@ def _process_module_builtin_defaults():
28512851
"instrument_autogen_agentchat_agents__assistant_agent",
28522852
)
28532853
_process_module_definition("mcp.client.session", "newrelic.hooks.adapter_mcp", "instrument_mcp_client_session")
2854+
_process_module_definition("mcp.server.fastmcp.tools.tool_manager", "newrelic.hooks.adapter_mcp", "instrument_mcp_server_fastmcp_tools_tool_manager")
2855+
28542856

28552857
_process_module_definition("structlog._base", "newrelic.hooks.logger_structlog", "instrument_structlog__base")
28562858
_process_module_definition("structlog._frames", "newrelic.hooks.logger_structlog", "instrument_structlog__frames")

newrelic/hooks/adapter_mcp.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from newrelic.common.object_names import callable_name
2020
from newrelic.common.object_wrapper import wrap_function_wrapper
2121
from newrelic.common.signature import bind_args
22+
from newrelic.core.config import global_settings
23+
2224

2325
_logger = logging.getLogger(__name__)
2426

@@ -28,6 +30,10 @@ async def wrap_call_tool(wrapped, instance, args, kwargs):
2830
if not transaction:
2931
return await wrapped(*args, **kwargs)
3032

33+
settings = transaction.settings if transaction.settings is not None else global_settings()
34+
if not settings.ai_monitoring.enabled:
35+
return await wrapped(*args, **kwargs)
36+
3137
func_name = callable_name(wrapped)
3238
bound_args = bind_args(wrapped, args, kwargs)
3339
tool_name = bound_args.get("name") or "tool"
@@ -42,6 +48,10 @@ async def wrap_read_resource(wrapped, instance, args, kwargs):
4248
if not transaction:
4349
return await wrapped(*args, **kwargs)
4450

51+
settings = transaction.settings if transaction.settings is not None else global_settings()
52+
if not settings.ai_monitoring.enabled:
53+
return await wrapped(*args, **kwargs)
54+
4555
func_name = callable_name(wrapped)
4656
bound_args = bind_args(wrapped, args, kwargs)
4757
# Set a default value in case we can't parse out the URI scheme successfully
@@ -64,6 +74,10 @@ async def wrap_get_prompt(wrapped, instance, args, kwargs):
6474
if not transaction:
6575
return await wrapped(*args, **kwargs)
6676

77+
settings = transaction.settings if transaction.settings is not None else global_settings()
78+
if not settings.ai_monitoring.enabled:
79+
return await wrapped(*args, **kwargs)
80+
6781
func_name = callable_name(wrapped)
6882
bound_args = bind_args(wrapped, args, kwargs)
6983
prompt_name = bound_args.get("name") or "prompt"
@@ -81,3 +95,9 @@ def instrument_mcp_client_session(module):
8195
wrap_function_wrapper(module, "ClientSession.read_resource", wrap_read_resource)
8296
if hasattr(module.ClientSession, "get_prompt"):
8397
wrap_function_wrapper(module, "ClientSession.get_prompt", wrap_get_prompt)
98+
99+
100+
def instrument_mcp_server_fastmcp_tools_tool_manager(module):
101+
if hasattr(module, "ToolManager"):
102+
if hasattr(module.ToolManager, "call_tool"):
103+
wrap_function_wrapper(module, "ToolManager.call_tool", wrap_call_tool)

tests/adapter_mcp/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"transaction_tracer.stack_trace_threshold": 0.0,
2323
"debug.log_data_collector_payloads": True,
2424
"debug.record_transaction_failure": True,
25+
"ai_monitoring.enabled": True,
2526
}
2627

2728
collector_agent_registration = collector_agent_registration_fixture(

tests/adapter_mcp/test_mcp.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
from fastmcp.client import Client
1717
from fastmcp.client.transports import FastMCPTransport
1818
from fastmcp.server.server import FastMCP
19-
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
19+
from mcp.server.fastmcp.tools import ToolManager
2020

21+
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
2122
from newrelic.api.background_task import background_task
2223

2324

@@ -49,13 +50,13 @@ def echo_prompt(message: str):
4950

5051

5152
@validate_transaction_metrics(
52-
"test_mcp:test_tool_tracing",
53+
"test_mcp:test_tool_tracing_via_client_session",
5354
scoped_metrics=[("Llm/tool/MCP/mcp.client.session:ClientSession.call_tool/add_exclamation", 1)],
5455
rollup_metrics=[("Llm/tool/MCP/mcp.client.session:ClientSession.call_tool/add_exclamation", 1)],
5556
background_task=True,
5657
)
5758
@background_task()
58-
def test_tool_tracing(loop, fastmcp_server):
59+
def test_tool_tracing_via_client_session(loop, fastmcp_server):
5960
async def _test():
6061
async with Client(transport=FastMCPTransport(fastmcp_server)) as client:
6162
# Call the MCP tool, so we can validate the trace naming is correct.
@@ -67,6 +68,26 @@ async def _test():
6768
loop.run_until_complete(_test())
6869

6970

71+
@validate_transaction_metrics(
72+
"test_mcp:test_tool_tracing_via_tool_manager",
73+
scoped_metrics=[("Llm/tool/MCP/mcp.server.fastmcp.tools.tool_manager:ToolManager.call_tool/add_exclamation", 1)],
74+
rollup_metrics=[("Llm/tool/MCP/mcp.server.fastmcp.tools.tool_manager:ToolManager.call_tool/add_exclamation", 1)],
75+
background_task=True,
76+
)
77+
@background_task()
78+
def test_tool_tracing_via_tool_manager(loop):
79+
async def _test():
80+
def add_exclamation(phrase):
81+
return f"{phrase}!"
82+
83+
manager = ToolManager()
84+
manager.add_tool(add_exclamation)
85+
result = await manager.call_tool("add_exclamation", {"phrase": "Python is awesome"})
86+
assert result == "Python is awesome!"
87+
88+
loop.run_until_complete(_test())
89+
90+
7091
# Separate out the test function to work with the transaction metrics validator
7192
def run_read_resources(loop, fastmcp_server, resource_uri):
7293
async def _test():

0 commit comments

Comments
 (0)