Skip to content

Commit b5fb791

Browse files
committed
Add tool manager spans to MCP instrumentation to support streaming.
1 parent c9845f0 commit b5fb791

File tree

4 files changed

+27
-68
lines changed

4 files changed

+27
-68
lines changed

newrelic/config.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2869,12 +2869,8 @@ def _process_module_builtin_defaults():
28692869
_process_module_definition("loguru._logger", "newrelic.hooks.logger_loguru", "instrument_loguru_logger")
28702870

28712871
_process_module_definition("mcp.client.session", "newrelic.hooks.adapter_mcp", "instrument_mcp_client_session")
2872-
_process_module_definition("mcp.server.fastmcp.server", "newrelic.hooks.adapter_mcp", "instrument_mcp_server_fastmcp_server")
2873-
_process_module_definition("mcp.server.fastmcp.utilities.func_metadata", "newrelic.hooks.adapter_mcp", "instrument_mcp_server_fastmcp_utilities_func_metadata")
2874-
28752872
_process_module_definition("mcp.server.fastmcp.tools.tool_manager", "newrelic.hooks.adapter_mcp", "instrument_mcp_server_fastmcp_tools_tool_manager")
2876-
_process_module_definition("mcp.server.fastmcp.tools.base", "newrelic.hooks.adapter_mcp", "instrument_mcp_server_fastmcp_tools_base")
2877-
_process_module_definition("mcp.server.lowlevel.server", "newrelic.hooks.adapter_mcp", "instrument_mcp_server_lowlevel_server")
2873+
28782874

28792875
_process_module_definition("structlog._base", "newrelic.hooks.logger_structlog", "instrument_structlog__base")
28802876
_process_module_definition("structlog._frames", "newrelic.hooks.logger_structlog", "instrument_structlog__frames")

newrelic/hooks/adapter_mcp.py

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,6 @@ async def wrap_call_tool(wrapped, instance, args, kwargs):
4343
return await wrapped(*args, **kwargs)
4444

4545

46-
async def wrap_call_tool1(wrapped, instance, args, kwargs):
47-
transaction = current_transaction()
48-
if not transaction:
49-
return await wrapped(*args, **kwargs)
50-
51-
func_name = callable_name(wrapped)
52-
bound_args = bind_args(wrapped, args, kwargs)
53-
tool_name = bound_args.get("name") or "tool"
54-
function_trace_name = f"{func_name}/{tool_name}"
55-
56-
with FunctionTrace(name=function_trace_name, group="Llm/tool/MCPPPPP", source=wrapped):
57-
return await wrapped(*args, **kwargs)
58-
59-
6046
async def wrap_read_resource(wrapped, instance, args, kwargs):
6147
transaction = current_transaction()
6248
if not transaction:
@@ -101,27 +87,6 @@ async def wrap_get_prompt(wrapped, instance, args, kwargs):
10187
return await wrapped(*args, **kwargs)
10288

10389

104-
async def wrap_call_fn_with_arg_validation(wrapped, instance, args, kwargs):
105-
with FunctionTrace(name="call_fn_with_arg_validation", group="Llm/argssss/MCP", source=wrapped):
106-
return await wrapped(*args, **kwargs)
107-
108-
109-
async def wrap_run(wrapped, instance, args, kwargs):
110-
with FunctionTrace(name="call_fn_with_arg_validation", group="Llm/MCP/base/tool/run", source=wrapped):
111-
return await wrapped(*args, **kwargs)
112-
113-
114-
async def wrap_tool_manager(wrapped, instance, args, kwargs):
115-
with FunctionTrace(name="call_tool", group="Llm/MCP/tool_manager", source=wrapped):
116-
return await wrapped(*args, **kwargs)
117-
118-
119-
async def wrap__handle_request(wrapped, instance, args, kwargs):
120-
func_name = callable_name(wrapped)
121-
with FunctionTrace(name="func_name", group="Llm/MCP/handle_request", source=wrapped):
122-
return await wrapped(*args, **kwargs)
123-
124-
12590
def instrument_mcp_client_session(module):
12691
if hasattr(module, "ClientSession"):
12792
if hasattr(module.ClientSession, "call_tool"):
@@ -132,31 +97,7 @@ def instrument_mcp_client_session(module):
13297
wrap_function_wrapper(module, "ClientSession.get_prompt", wrap_get_prompt)
13398

13499

135-
def instrument_mcp_server_fastmcp_server(module):
136-
if hasattr(module, "FastMCP"):
137-
if hasattr(module.FastMCP, "call_tool"):
138-
wrap_function_wrapper(module, "FastMCP.call_tool", wrap_call_tool1)
139-
140-
141-
def instrument_mcp_server_lowlevel_server(module):
142-
wrap_function_wrapper(module, "Server._handle_request", wrap__handle_request)
143-
144-
145-
def instrument_mcp_server_fastmcp_utilities_func_metadata(module):
146-
pass
147-
# if hasattr(module, "FuncMetadata"):
148-
# if hasattr(module.FuncMetadata, "call_fn_with_arg_validation"):
149-
# wrap_function_wrapper(module, "FuncMetadata.call_fn_with_arg_validation", wrap_call_fn_with_arg_validation)
150-
151-
152-
def instrument_mcp_server_fastmcp_tools_base(module):
153-
pass
154-
# if hasattr(module, "Tool"):
155-
# if hasattr(module.Tool, "run"):
156-
# wrap_function_wrapper(module, "Tool.run", wrap_run)
157-
158-
159100
def instrument_mcp_server_fastmcp_tools_tool_manager(module):
160101
if hasattr(module, "ToolManager"):
161102
if hasattr(module.ToolManager, "call_tool"):
162-
wrap_function_wrapper(module, "ToolManager.call_tool", wrap_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)