Skip to content

Commit b2caa75

Browse files
mitchh456claude
andauthored
Use Job.id property instead of removed get_id() (#851)
* Use Job.id property instead of removed get_id() method rq v2.7 refactored Job.id from `id = property(get_id, set_id)` to a `@property` decorator, removing get_id() as a callable method. Job.id has been available since rq v0.5.0 so this is backwards compatible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Support FastMCP 3.x while maintaining 2.x backwards compatibility (#852) FastMCP 3.0 replaced private _call_tool_mcp()/_list_tools_mcp() with public call_tool()/list_tools() methods, and changed get_tool() to return None instead of raising when a tool is not found. - Update instrumentation to handle get_tool() returning None - Update tests with compat helpers that use the correct API based on the installed fastmcp version - Remove the fastmcp<3 version pin from tox.ini (no longer needed) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 58a6eed commit b2caa75

3 files changed

Lines changed: 50 additions & 24 deletions

File tree

src/scout_apm/fastmcp.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ async def on_call_tool(self, context, call_next):
5252
# Add rich metadata from tool object via context
5353
try:
5454
tool = await context.fastmcp_context.fastmcp.get_tool(tool_name)
55-
self._tag_tool_metadata(tracked_request, tool)
55+
if tool is not None:
56+
self._tag_tool_metadata(tracked_request, tool)
5657
except Exception as exc:
5758
# Tool not found or other error - continue without metadata
5859
logger.warning(f"Unable to fetch tool metadata for {tool_name}: {exc}")

src/scout_apm/rq.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def wrap_perform(wrapped, instance, args, kwargs):
6666

6767
tracked_request = TrackedRequest.instance()
6868
tracked_request.is_real_request = True
69-
tracked_request.tag("task_id", instance.get_id())
69+
tracked_request.tag("task_id", instance.id)
7070
tracked_request.tag("queue", instance.origin)
7171
# rq strips tzinfo from enqueued_at during serde in at least some cases
7272
# internally everything uses UTC naive datetimes, so we operate on that

tests/integration/test_fastmcp.py

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,39 @@ def parse_version(v):
2121

2222
from scout_apm.fastmcp import ScoutMiddleware
2323
except (ImportError, TypeError):
24-
# fastmcp has compatibility issues with version <2.13.0
25-
# This is due to us using internal methods to test the middleware hooks
26-
# These internal methods were renamed in 2.13.0
2724
fastmcp_version = "0.0.0"
28-
pass
25+
26+
_fastmcp_version = parse_version(fastmcp_version)
2927

3028
pytestmark = pytest.mark.skipif(
31-
parse_version(fastmcp_version) < (2, 13, 0) or sys.version_info < (3, 10),
29+
_fastmcp_version < (2, 13, 0) or sys.version_info < (3, 10),
3230
reason="These tests require fastMCP 2.13.0+ and Python 3.10+",
3331
)
3432

3533

34+
async def _call_tool(mcp, name, arguments):
35+
"""
36+
Call a tool on a FastMCP server, compatible with both 2.x and 3.x.
37+
38+
Returns (content_blocks, metadata) for uniform access.
39+
"""
40+
if _fastmcp_version >= (3,):
41+
result = await mcp.call_tool(name, arguments)
42+
return result.content, result.meta
43+
else:
44+
return await mcp._call_tool_mcp(name, arguments)
45+
46+
47+
async def _list_tools(mcp):
48+
"""
49+
List tools on a FastMCP server, compatible with both 2.x and 3.x.
50+
"""
51+
if _fastmcp_version >= (3,):
52+
return await mcp.list_tools()
53+
else:
54+
return await mcp._list_tools_mcp()
55+
56+
3657
@contextmanager
3758
def server_with_scout(scout_config=None):
3859
"""
@@ -70,14 +91,14 @@ def add_numbers(a: int, b: int) -> int:
7091
return a + b
7192

7293
# Verify tool is registered
73-
tools_list = await mcp._list_tools_mcp()
94+
tools_list = await _list_tools(mcp)
7495
assert len(tools_list) == 1
7596
assert tools_list[0].name == "add_numbers"
7697

7798
# Simulate tool execution using the MCP protocol method
78-
result = await mcp._call_tool_mcp("add_numbers", {"a": 5, "b": 3})
79-
# result is a tuple: (content_blocks, metadata)
80-
content_blocks, metadata = result
99+
content_blocks, metadata = await _call_tool(
100+
mcp, "add_numbers", {"a": 5, "b": 3}
101+
)
81102
assert len(content_blocks) == 1
82103
assert content_blocks[0].text == "8"
83104

@@ -100,8 +121,10 @@ async def async_multiply(a: int, b: int) -> int:
100121
return a * b
101122

102123
# Simulate tool execution
103-
result, metadata = await mcp._call_tool_mcp("async_multiply", {"a": 4, "b": 7})
104-
assert result[0].text == "28"
124+
content_blocks, metadata = await _call_tool(
125+
mcp, "async_multiply", {"a": 4, "b": 7}
126+
)
127+
assert content_blocks[0].text == "28"
105128

106129
# Verify tracking
107130
assert len(tracked_requests) == 1
@@ -130,8 +153,8 @@ def search_database(query: str) -> list:
130153
return [{"id": 1, "name": "result"}]
131154

132155
# Execute tool
133-
result, metadata = await mcp._call_tool_mcp("search_db", {"query": "test"})
134-
assert len(result) == 1
156+
content_blocks, metadata = await _call_tool(mcp, "search_db", {"query": "test"})
157+
assert len(content_blocks) == 1
135158

136159
# Verify metadata tags
137160
assert len(tracked_requests) == 1
@@ -158,13 +181,15 @@ def process_data(data: str, password: str, count: int) -> dict:
158181
return {"processed": True, "length": len(data)}
159182

160183
# Execute tool with sensitive parameter
161-
result, metadata = await mcp._call_tool_mcp(
162-
"process_data", {"data": "test data", "password": "secret123", "count": 5}
184+
content_blocks, metadata = await _call_tool(
185+
mcp,
186+
"process_data",
187+
{"data": "test data", "password": "secret123", "count": 5},
163188
)
164189
# FastMCP returns list of ContentBlock, need to parse the JSON
165190
import json
166191

167-
result_data = json.loads(result[0].text)
192+
result_data = json.loads(content_blocks[0].text)
168193
assert result_data["processed"] is True
169194

170195
# Verify arguments are tagged
@@ -195,7 +220,7 @@ def divide_numbers(a: float, b: float) -> float:
195220

196221
# Execute tool that raises an error
197222
with pytest.raises(ToolError, match="Division by zero"):
198-
await mcp._call_tool_mcp("divide_numbers", {"a": 10, "b": 0})
223+
await _call_tool(mcp, "divide_numbers", {"a": 10, "b": 0})
199224

200225
# Verify error tracking
201226
assert len(tracked_requests) == 1
@@ -214,9 +239,9 @@ def echo(message: str) -> str:
214239
return message
215240

216241
# Execute multiple times
217-
await mcp._call_tool_mcp("echo", {"message": "first"})
218-
await mcp._call_tool_mcp("echo", {"message": "second"})
219-
await mcp._call_tool_mcp("echo", {"message": "third"})
242+
await _call_tool(mcp, "echo", {"message": "first"})
243+
await _call_tool(mcp, "echo", {"message": "second"})
244+
await _call_tool(mcp, "echo", {"message": "third"})
220245

221246
# Should have 3 separate tracked requests
222247
assert len(tracked_requests) == 3
@@ -234,8 +259,8 @@ def monitored_tool() -> str:
234259
"""This should not be tracked."""
235260
return "result"
236261

237-
result, metadata = await mcp._call_tool_mcp("monitored_tool", {})
238-
assert result[0].text == "result"
262+
content_blocks, metadata = await _call_tool(mcp, "monitored_tool", {})
263+
assert content_blocks[0].text == "result"
239264

240265
# Should not track when monitor is disabled
241266
assert len(tracked_requests) == 0

0 commit comments

Comments
 (0)