Skip to content

Commit f7b8710

Browse files
Add MCP tool instrumentation (#1393)
* Add tool call tracing. * Add safeguarding. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent a311b33 commit f7b8710

File tree

5 files changed

+125
-0
lines changed

5 files changed

+125
-0
lines changed

newrelic/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2861,6 +2861,9 @@ def _process_module_builtin_defaults():
28612861

28622862
_process_module_definition("loguru", "newrelic.hooks.logger_loguru", "instrument_loguru")
28632863
_process_module_definition("loguru._logger", "newrelic.hooks.logger_loguru", "instrument_loguru_logger")
2864+
2865+
_process_module_definition("mcp.client.session", "newrelic.hooks.adapter_mcp", "instrument_mcp_client_session")
2866+
28642867
_process_module_definition("structlog._base", "newrelic.hooks.logger_structlog", "instrument_structlog__base")
28652868
_process_module_definition("structlog._frames", "newrelic.hooks.logger_structlog", "instrument_structlog__frames")
28662869
_process_module_definition("paste.httpserver", "newrelic.hooks.adapter_paste", "instrument_paste_httpserver")

newrelic/hooks/adapter_mcp.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from newrelic.api.function_trace import FunctionTrace
16+
from newrelic.api.transaction import current_transaction
17+
from newrelic.common.object_names import callable_name
18+
from newrelic.common.object_wrapper import wrap_function_wrapper
19+
from newrelic.common.signature import bind_args
20+
21+
22+
async def wrap_call_tool(wrapped, instance, args, kwargs):
23+
transaction = current_transaction()
24+
if not transaction:
25+
return await wrapped(*args, **kwargs)
26+
27+
bound_args = bind_args(wrapped, args, kwargs)
28+
tool_name = bound_args.get("name") or "tool"
29+
func_name = callable_name(wrapped)
30+
function_trace_name = f"{func_name}/{tool_name}"
31+
32+
with FunctionTrace(name=function_trace_name, source=wrapped):
33+
return await wrapped(*args, **kwargs)
34+
35+
36+
def instrument_mcp_client_session(module):
37+
if hasattr(module, "ClientSession"):
38+
if hasattr(module.ClientSession, "call_tool"):
39+
wrap_function_wrapper(module, "ClientSession.call_tool", wrap_call_tool)

tests/adapter_mcp/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from testing_support.fixture.event_loop import event_loop as loop
16+
from testing_support.fixtures import collector_agent_registration_fixture, collector_available_fixture
17+
18+
_default_settings = {
19+
"package_reporting.enabled": False, # Turn off package reporting for testing as it causes slowdowns.
20+
"transaction_tracer.explain_threshold": 0.0,
21+
"transaction_tracer.transaction_threshold": 0.0,
22+
"transaction_tracer.stack_trace_threshold": 0.0,
23+
"debug.log_data_collector_payloads": True,
24+
"debug.record_transaction_failure": True,
25+
}
26+
27+
collector_agent_registration = collector_agent_registration_fixture(
28+
app_name="Python Agent Test (adapter_mcp)", default_settings=_default_settings
29+
)

tests/adapter_mcp/test_tools.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
from fastmcp.client import Client
17+
from fastmcp.client.transports import FastMCPTransport
18+
from fastmcp.server.server import FastMCP
19+
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
20+
21+
from newrelic.api.background_task import background_task
22+
23+
24+
@pytest.fixture
25+
def fastmcp_server():
26+
server = FastMCP("Test Tools")
27+
28+
@server.tool()
29+
def add_exclamation(phrase):
30+
return f"{phrase}!"
31+
32+
return server
33+
34+
35+
@validate_transaction_metrics(
36+
"test_tools:test_tool_tracing",
37+
scoped_metrics=[("Function/mcp.client.session:ClientSession.call_tool/add_exclamation", 1)],
38+
rollup_metrics=[("Function/mcp.client.session:ClientSession.call_tool/add_exclamation", 1)],
39+
background_task=True,
40+
)
41+
@background_task()
42+
def test_tool_tracing(loop, fastmcp_server):
43+
async def _test():
44+
async with Client(transport=FastMCPTransport(fastmcp_server)) as client:
45+
# Call the MCP tool, so we can validate the trace naming is correct.
46+
result = await client.call_tool("add_exclamation", {"phrase": "Python is awesome"})
47+
48+
content = str(result[0])
49+
assert "Python is awesome!" in content
50+
51+
loop.run_until_complete(_test())

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ envlist =
8383
python-adapter_gunicorn-{py37,py38,py39,py310,py311,py312,py313}-aiohttp03-gunicornlatest,
8484
python-adapter_hypercorn-{py38,py39,py310,py311,py312,py313}-hypercornlatest,
8585
python-adapter_hypercorn-py38-hypercorn{0010,0011,0012,0013},
86+
python-adapter_mcp-{py310,py311,py312,py313,pypy310},
8687
python-adapter_uvicorn-{py37,py38,py39,py310,py311,py312,py313}-uvicornlatest,
8788
python-adapter_uvicorn-py38-uvicorn014,
8889
python-adapter_waitress-{py37,py38,py39,py310,py311,py312,py313}-waitresslatest,
@@ -210,6 +211,7 @@ deps =
210211
adapter_hypercorn-hypercorn0011: hypercorn[h3]<0.12
211212
adapter_hypercorn-hypercorn0010: hypercorn[h3]<0.11
212213
adapter_hypercorn: niquests
214+
adapter_mcp: fastmcp
213215
adapter_uvicorn-uvicorn014: uvicorn<0.15
214216
adapter_uvicorn-uvicornlatest: uvicorn
215217
adapter_uvicorn: typing-extensions
@@ -477,6 +479,7 @@ changedir =
477479
adapter_gevent: tests/adapter_gevent
478480
adapter_gunicorn: tests/adapter_gunicorn
479481
adapter_hypercorn: tests/adapter_hypercorn
482+
adapter_mcp: tests/adapter_mcp
480483
adapter_uvicorn: tests/adapter_uvicorn
481484
adapter_waitress: tests/adapter_waitress
482485
agent_features: tests/agent_features

0 commit comments

Comments
 (0)