sentry-sdk 0.7.5__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +289 -0
  8. sentry_sdk/_types.py +338 -0
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +134 -0
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +141 -0
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +112 -68
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,566 @@
1
+ """
2
+ Sentry integration for MCP (Model Context Protocol) servers.
3
+
4
+ This integration instruments MCP servers to create spans for tool, prompt,
5
+ and resource handler execution, and captures errors that occur during execution.
6
+
7
+ Supports the low-level `mcp.server.lowlevel.Server` API.
8
+ """
9
+
10
+ import inspect
11
+ from functools import wraps
12
+ from typing import TYPE_CHECKING
13
+
14
+ import sentry_sdk
15
+ from sentry_sdk.ai.utils import get_start_span_function
16
+ from sentry_sdk.consts import OP, SPANDATA
17
+ from sentry_sdk.integrations import Integration, DidNotEnable
18
+ from sentry_sdk.utils import safe_serialize
19
+ from sentry_sdk.scope import should_send_default_pii
20
+
21
+ try:
22
+ from mcp.server.lowlevel import Server # type: ignore[import-not-found]
23
+ from mcp.server.lowlevel.server import request_ctx # type: ignore[import-not-found]
24
+ except ImportError:
25
+ raise DidNotEnable("MCP SDK not installed")
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from typing import Any, Callable, Optional
30
+
31
+
32
+ class MCPIntegration(Integration):
33
+ identifier = "mcp"
34
+ origin = "auto.ai.mcp"
35
+
36
+ def __init__(self, include_prompts=True):
37
+ # type: (bool) -> None
38
+ """
39
+ Initialize the MCP integration.
40
+
41
+ Args:
42
+ include_prompts: Whether to include prompts (tool results and prompt content)
43
+ in span data. Requires send_default_pii=True. Default is True.
44
+ """
45
+ self.include_prompts = include_prompts
46
+
47
+ @staticmethod
48
+ def setup_once():
49
+ # type: () -> None
50
+ """
51
+ Patches MCP server classes to instrument handler execution.
52
+ """
53
+ _patch_lowlevel_server()
54
+
55
+
56
+ def _get_request_context_data():
57
+ # type: () -> tuple[Optional[str], Optional[str], str]
58
+ """
59
+ Extract request ID, session ID, and MCP transport type from the request context.
60
+
61
+ Returns:
62
+ Tuple of (request_id, session_id, mcp_transport).
63
+ - request_id: May be None if not available
64
+ - session_id: May be None if not available
65
+ - mcp_transport: "http", "sse", "stdio"
66
+ """
67
+ request_id = None # type: Optional[str]
68
+ session_id = None # type: Optional[str]
69
+ mcp_transport = "stdio" # type: str
70
+
71
+ try:
72
+ ctx = request_ctx.get()
73
+
74
+ if ctx is not None:
75
+ request_id = ctx.request_id
76
+ if hasattr(ctx, "request") and ctx.request is not None:
77
+ request = ctx.request
78
+ # Detect transport type by checking request characteristics
79
+ if hasattr(request, "query_params") and request.query_params.get(
80
+ "session_id"
81
+ ):
82
+ # SSE transport uses query parameter
83
+ mcp_transport = "sse"
84
+ session_id = request.query_params.get("session_id")
85
+ elif hasattr(request, "headers") and request.headers.get(
86
+ "mcp-session-id"
87
+ ):
88
+ # StreamableHTTP transport uses header
89
+ mcp_transport = "http"
90
+ session_id = request.headers.get("mcp-session-id")
91
+
92
+ except LookupError:
93
+ # No request context available - default to stdio
94
+ pass
95
+
96
+ return request_id, session_id, mcp_transport
97
+
98
+
99
+ def _get_span_config(handler_type, item_name):
100
+ # type: (str, str) -> tuple[str, str, str, Optional[str]]
101
+ """
102
+ Get span configuration based on handler type.
103
+
104
+ Returns:
105
+ Tuple of (span_data_key, span_name, mcp_method_name, result_data_key)
106
+ Note: result_data_key is None for resources
107
+ """
108
+ if handler_type == "tool":
109
+ span_data_key = SPANDATA.MCP_TOOL_NAME
110
+ mcp_method_name = "tools/call"
111
+ result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT
112
+ elif handler_type == "prompt":
113
+ span_data_key = SPANDATA.MCP_PROMPT_NAME
114
+ mcp_method_name = "prompts/get"
115
+ result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT
116
+ else: # resource
117
+ span_data_key = SPANDATA.MCP_RESOURCE_URI
118
+ mcp_method_name = "resources/read"
119
+ result_data_key = None # Resources don't capture result content
120
+
121
+ span_name = f"{mcp_method_name} {item_name}"
122
+ return span_data_key, span_name, mcp_method_name, result_data_key
123
+
124
+
125
+ def _set_span_input_data(
126
+ span,
127
+ handler_name,
128
+ span_data_key,
129
+ mcp_method_name,
130
+ arguments,
131
+ request_id,
132
+ session_id,
133
+ mcp_transport,
134
+ ):
135
+ # type: (Any, str, str, str, dict[str, Any], Optional[str], Optional[str], str) -> None
136
+ """Set input span data for MCP handlers."""
137
+
138
+ # Set handler identifier
139
+ span.set_data(span_data_key, handler_name)
140
+ span.set_data(SPANDATA.MCP_METHOD_NAME, mcp_method_name)
141
+
142
+ # Set transport/MCP transport type
143
+ span.set_data(
144
+ SPANDATA.NETWORK_TRANSPORT, "pipe" if mcp_transport == "stdio" else "tcp"
145
+ )
146
+ span.set_data(SPANDATA.MCP_TRANSPORT, mcp_transport)
147
+
148
+ # Set request_id if provided
149
+ if request_id:
150
+ span.set_data(SPANDATA.MCP_REQUEST_ID, request_id)
151
+
152
+ # Set session_id if provided
153
+ if session_id:
154
+ span.set_data(SPANDATA.MCP_SESSION_ID, session_id)
155
+
156
+ # Set request arguments (excluding common request context objects)
157
+ for k, v in arguments.items():
158
+ span.set_data(f"mcp.request.argument.{k}", safe_serialize(v))
159
+
160
+
161
+ def _extract_tool_result_content(result):
162
+ # type: (Any) -> Any
163
+ """
164
+ Extract meaningful content from MCP tool result.
165
+
166
+ Tool handlers can return:
167
+ - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict)
168
+ - dict (StructuredContent): Return as-is
169
+ - Iterable (UnstructuredContent): Extract text from content blocks
170
+ """
171
+ if result is None:
172
+ return None
173
+
174
+ # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent)
175
+ if isinstance(result, tuple) and len(result) == 2:
176
+ # Return the structured content (2nd element)
177
+ return result[1]
178
+
179
+ # Handle StructuredContent: dict
180
+ if isinstance(result, dict):
181
+ return result
182
+
183
+ # Handle UnstructuredContent: iterable of ContentBlock objects
184
+ # Try to extract text content
185
+ if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)):
186
+ texts = []
187
+ try:
188
+ for item in result:
189
+ # Try to get text attribute from ContentBlock objects
190
+ if hasattr(item, "text"):
191
+ texts.append(item.text)
192
+ elif isinstance(item, dict) and "text" in item:
193
+ texts.append(item["text"])
194
+ except Exception:
195
+ # If extraction fails, return the original
196
+ return result
197
+ return " ".join(texts) if texts else result
198
+
199
+ return result
200
+
201
+
202
+ def _set_span_output_data(span, result, result_data_key, handler_type):
203
+ # type: (Any, Any, Optional[str], str) -> None
204
+ """Set output span data for MCP handlers."""
205
+ if result is None:
206
+ return
207
+
208
+ # Get integration to check PII settings
209
+ integration = sentry_sdk.get_client().get_integration(MCPIntegration)
210
+ if integration is None:
211
+ return
212
+
213
+ # Check if we should include sensitive data
214
+ should_include_data = should_send_default_pii() and integration.include_prompts
215
+
216
+ # For tools, extract the meaningful content
217
+ if handler_type == "tool":
218
+ extracted = _extract_tool_result_content(result)
219
+ if extracted is not None and should_include_data:
220
+ span.set_data(result_data_key, safe_serialize(extracted))
221
+ # Set content count if result is a dict
222
+ if isinstance(extracted, dict):
223
+ span.set_data(SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted))
224
+ elif handler_type == "prompt":
225
+ # For prompts, count messages and set role/content only for single-message prompts
226
+ try:
227
+ messages = None # type: Optional[list[str]]
228
+ message_count = 0
229
+
230
+ # Check if result has messages attribute (GetPromptResult)
231
+ if hasattr(result, "messages") and result.messages:
232
+ messages = result.messages
233
+ message_count = len(messages)
234
+ # Also check if result is a dict with messages
235
+ elif isinstance(result, dict) and result.get("messages"):
236
+ messages = result["messages"]
237
+ message_count = len(messages)
238
+
239
+ # Always set message count if we found messages
240
+ if message_count > 0:
241
+ span.set_data(SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count)
242
+
243
+ # Only set role and content for single-message prompts if PII is allowed
244
+ if message_count == 1 and should_include_data and messages:
245
+ first_message = messages[0]
246
+ # Extract role
247
+ role = None
248
+ if hasattr(first_message, "role"):
249
+ role = first_message.role
250
+ elif isinstance(first_message, dict) and "role" in first_message:
251
+ role = first_message["role"]
252
+
253
+ if role:
254
+ span.set_data(SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role)
255
+
256
+ # Extract content text
257
+ content_text = None
258
+ if hasattr(first_message, "content"):
259
+ msg_content = first_message.content
260
+ # Content can be a TextContent object or similar
261
+ if hasattr(msg_content, "text"):
262
+ content_text = msg_content.text
263
+ elif isinstance(msg_content, dict) and "text" in msg_content:
264
+ content_text = msg_content["text"]
265
+ elif isinstance(msg_content, str):
266
+ content_text = msg_content
267
+ elif isinstance(first_message, dict) and "content" in first_message:
268
+ msg_content = first_message["content"]
269
+ if isinstance(msg_content, dict) and "text" in msg_content:
270
+ content_text = msg_content["text"]
271
+ elif isinstance(msg_content, str):
272
+ content_text = msg_content
273
+
274
+ if content_text:
275
+ span.set_data(result_data_key, content_text)
276
+ except Exception:
277
+ # Silently ignore if we can't extract message info
278
+ pass
279
+ # Resources don't capture result content (result_data_key is None)
280
+
281
+
282
+ # Handler data preparation and wrapping
283
+
284
+
285
+ def _prepare_handler_data(handler_type, original_args):
286
+ # type: (str, tuple[Any, ...]) -> tuple[str, dict[str, Any], str, str, str, Optional[str]]
287
+ """
288
+ Prepare common handler data for both async and sync wrappers.
289
+
290
+ Returns:
291
+ Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key)
292
+ """
293
+ # Extract handler-specific data based on handler type
294
+ if handler_type == "tool":
295
+ handler_name = original_args[0] # tool_name
296
+ arguments = original_args[1] if len(original_args) > 1 else {}
297
+ elif handler_type == "prompt":
298
+ handler_name = original_args[0] # name
299
+ arguments = original_args[1] if len(original_args) > 1 else {}
300
+ # Include name in arguments dict for span data
301
+ arguments = {"name": handler_name, **(arguments or {})}
302
+ else: # resource
303
+ uri = original_args[0]
304
+ handler_name = str(uri) if uri else "unknown"
305
+ arguments = {}
306
+
307
+ # Get span configuration
308
+ span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config(
309
+ handler_type, handler_name
310
+ )
311
+
312
+ return (
313
+ handler_name,
314
+ arguments,
315
+ span_data_key,
316
+ span_name,
317
+ mcp_method_name,
318
+ result_data_key,
319
+ )
320
+
321
+
322
+ async def _async_handler_wrapper(handler_type, func, original_args):
323
+ # type: (str, Callable[..., Any], tuple[Any, ...]) -> Any
324
+ """
325
+ Async wrapper for MCP handlers.
326
+
327
+ Args:
328
+ handler_type: "tool", "prompt", or "resource"
329
+ func: The async handler function to wrap
330
+ original_args: Original arguments passed to the handler
331
+ """
332
+ (
333
+ handler_name,
334
+ arguments,
335
+ span_data_key,
336
+ span_name,
337
+ mcp_method_name,
338
+ result_data_key,
339
+ ) = _prepare_handler_data(handler_type, original_args)
340
+
341
+ # Start span and execute
342
+ with get_start_span_function()(
343
+ op=OP.MCP_SERVER,
344
+ name=span_name,
345
+ origin=MCPIntegration.origin,
346
+ ) as span:
347
+ # Get request ID, session ID, and transport from context
348
+ request_id, session_id, mcp_transport = _get_request_context_data()
349
+
350
+ # Set input span data
351
+ _set_span_input_data(
352
+ span,
353
+ handler_name,
354
+ span_data_key,
355
+ mcp_method_name,
356
+ arguments,
357
+ request_id,
358
+ session_id,
359
+ mcp_transport,
360
+ )
361
+
362
+ # For resources, extract and set protocol
363
+ if handler_type == "resource":
364
+ uri = original_args[0]
365
+ protocol = None
366
+ if hasattr(uri, "scheme"):
367
+ protocol = uri.scheme
368
+ elif handler_name and "://" in handler_name:
369
+ protocol = handler_name.split("://")[0]
370
+ if protocol:
371
+ span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
372
+
373
+ try:
374
+ # Execute the async handler
375
+ result = await func(*original_args)
376
+ except Exception as e:
377
+ # Set error flag for tools
378
+ if handler_type == "tool":
379
+ span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
380
+ sentry_sdk.capture_exception(e)
381
+ raise
382
+
383
+ _set_span_output_data(span, result, result_data_key, handler_type)
384
+ return result
385
+
386
+
387
+ def _sync_handler_wrapper(handler_type, func, original_args):
388
+ # type: (str, Callable[..., Any], tuple[Any, ...]) -> Any
389
+ """
390
+ Sync wrapper for MCP handlers.
391
+
392
+ Args:
393
+ handler_type: "tool", "prompt", or "resource"
394
+ func: The sync handler function to wrap
395
+ original_args: Original arguments passed to the handler
396
+ """
397
+ (
398
+ handler_name,
399
+ arguments,
400
+ span_data_key,
401
+ span_name,
402
+ mcp_method_name,
403
+ result_data_key,
404
+ ) = _prepare_handler_data(handler_type, original_args)
405
+
406
+ # Start span and execute
407
+ with get_start_span_function()(
408
+ op=OP.MCP_SERVER,
409
+ name=span_name,
410
+ origin=MCPIntegration.origin,
411
+ ) as span:
412
+ # Get request ID, session ID, and transport from context
413
+ request_id, session_id, mcp_transport = _get_request_context_data()
414
+
415
+ # Set input span data
416
+ _set_span_input_data(
417
+ span,
418
+ handler_name,
419
+ span_data_key,
420
+ mcp_method_name,
421
+ arguments,
422
+ request_id,
423
+ session_id,
424
+ mcp_transport,
425
+ )
426
+
427
+ # For resources, extract and set protocol
428
+ if handler_type == "resource":
429
+ uri = original_args[0]
430
+ protocol = None
431
+ if hasattr(uri, "scheme"):
432
+ protocol = uri.scheme
433
+ elif handler_name and "://" in handler_name:
434
+ protocol = handler_name.split("://")[0]
435
+ if protocol:
436
+ span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
437
+
438
+ try:
439
+ # Execute the sync handler
440
+ result = func(*original_args)
441
+ except Exception as e:
442
+ # Set error flag for tools
443
+ if handler_type == "tool":
444
+ span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
445
+ sentry_sdk.capture_exception(e)
446
+ raise
447
+
448
+ _set_span_output_data(span, result, result_data_key, handler_type)
449
+ return result
450
+
451
+
452
+ def _create_instrumented_handler(handler_type, func):
453
+ # type: (str, Callable[..., Any]) -> Callable[..., Any]
454
+ """
455
+ Create an instrumented version of a handler function (async or sync).
456
+
457
+ This function wraps the user's handler with a runtime wrapper that will create
458
+ Sentry spans and capture metrics when the handler is actually called.
459
+
460
+ The wrapper preserves the async/sync nature of the original function, which is
461
+ critical for Python's async/await to work correctly.
462
+
463
+ Args:
464
+ handler_type: "tool", "prompt", or "resource" - determines span configuration
465
+ func: The handler function to instrument (async or sync)
466
+
467
+ Returns:
468
+ A wrapped version of func that creates Sentry spans on execution
469
+ """
470
+ if inspect.iscoroutinefunction(func):
471
+
472
+ @wraps(func)
473
+ async def async_wrapper(*args):
474
+ # type: (*Any) -> Any
475
+ return await _async_handler_wrapper(handler_type, func, args)
476
+
477
+ return async_wrapper
478
+ else:
479
+
480
+ @wraps(func)
481
+ def sync_wrapper(*args):
482
+ # type: (*Any) -> Any
483
+ return _sync_handler_wrapper(handler_type, func, args)
484
+
485
+ return sync_wrapper
486
+
487
+
488
+ def _create_instrumented_decorator(
489
+ original_decorator, handler_type, *decorator_args, **decorator_kwargs
490
+ ):
491
+ # type: (Callable[..., Any], str, *Any, **Any) -> Callable[..., Any]
492
+ """
493
+ Create an instrumented version of an MCP decorator.
494
+
495
+ This function intercepts MCP decorators (like @server.call_tool()) and injects
496
+ Sentry instrumentation into the handler registration flow. The returned decorator
497
+ will:
498
+ 1. Receive the user's handler function
499
+ 2. Wrap it with instrumentation via _create_instrumented_handler
500
+ 3. Pass the instrumented version to the original MCP decorator
501
+
502
+ This ensures that when the handler is called at runtime, it's already wrapped
503
+ with Sentry spans and metrics collection.
504
+
505
+ Args:
506
+ original_decorator: The original MCP decorator method (e.g., Server.call_tool)
507
+ handler_type: "tool", "prompt", or "resource" - determines span configuration
508
+ decorator_args: Positional arguments to pass to the original decorator (e.g., self)
509
+ decorator_kwargs: Keyword arguments to pass to the original decorator
510
+
511
+ Returns:
512
+ A decorator function that instruments handlers before registering them
513
+ """
514
+
515
+ def instrumented_decorator(func):
516
+ # type: (Callable[..., Any]) -> Callable[..., Any]
517
+ # First wrap the handler with instrumentation
518
+ instrumented_func = _create_instrumented_handler(handler_type, func)
519
+ # Then register it with the original MCP decorator
520
+ return original_decorator(*decorator_args, **decorator_kwargs)(
521
+ instrumented_func
522
+ )
523
+
524
+ return instrumented_decorator
525
+
526
+
527
+ def _patch_lowlevel_server():
528
+ # type: () -> None
529
+ """
530
+ Patches the mcp.server.lowlevel.Server class to instrument handler execution.
531
+ """
532
+ # Patch call_tool decorator
533
+ original_call_tool = Server.call_tool
534
+
535
+ def patched_call_tool(self, **kwargs):
536
+ # type: (Server, **Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]
537
+ """Patched version of Server.call_tool that adds Sentry instrumentation."""
538
+ return lambda func: _create_instrumented_decorator(
539
+ original_call_tool, "tool", self, **kwargs
540
+ )(func)
541
+
542
+ Server.call_tool = patched_call_tool
543
+
544
+ # Patch get_prompt decorator
545
+ original_get_prompt = Server.get_prompt
546
+
547
+ def patched_get_prompt(self):
548
+ # type: (Server) -> Callable[[Callable[..., Any]], Callable[..., Any]]
549
+ """Patched version of Server.get_prompt that adds Sentry instrumentation."""
550
+ return lambda func: _create_instrumented_decorator(
551
+ original_get_prompt, "prompt", self
552
+ )(func)
553
+
554
+ Server.get_prompt = patched_get_prompt
555
+
556
+ # Patch read_resource decorator
557
+ original_read_resource = Server.read_resource
558
+
559
+ def patched_read_resource(self):
560
+ # type: (Server) -> Callable[[Callable[..., Any]], Callable[..., Any]]
561
+ """Patched version of Server.read_resource that adds Sentry instrumentation."""
562
+ return lambda func: _create_instrumented_decorator(
563
+ original_read_resource, "resource", self
564
+ )(func)
565
+
566
+ Server.read_resource = patched_read_resource
@@ -1,35 +1,13 @@
1
- from __future__ import absolute_import
2
-
3
- from sentry_sdk.hub import Hub
1
+ import sentry_sdk
4
2
  from sentry_sdk.integrations import Integration
5
3
  from sentry_sdk.scope import add_global_event_processor
4
+ from sentry_sdk.utils import _get_installed_modules
6
5
 
7
- if False:
8
- from typing import Any
9
- from typing import Dict
10
- from typing import Tuple
11
- from typing import Iterator
12
-
13
- _installed_modules = None
14
-
15
-
16
- def _generate_installed_modules():
17
- # type: () -> Iterator[Tuple[str, str]]
18
- try:
19
- import pkg_resources
20
- except ImportError:
21
- return
6
+ from typing import TYPE_CHECKING
22
7
 
23
- for info in pkg_resources.working_set:
24
- yield info.key, info.version
25
-
26
-
27
- def _get_installed_modules():
28
- # type: () -> Dict[str, str]
29
- global _installed_modules
30
- if _installed_modules is None:
31
- _installed_modules = dict(_generate_installed_modules())
32
- return _installed_modules
8
+ if TYPE_CHECKING:
9
+ from typing import Any
10
+ from sentry_sdk._types import Event
33
11
 
34
12
 
35
13
  class ModulesIntegration(Integration):
@@ -40,7 +18,12 @@ class ModulesIntegration(Integration):
40
18
  # type: () -> None
41
19
  @add_global_event_processor
42
20
  def processor(event, hint):
43
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
44
- if Hub.current.get_integration(ModulesIntegration) is not None:
45
- event["modules"] = dict(_get_installed_modules())
21
+ # type: (Event, Any) -> Event
22
+ if event.get("type") == "transaction":
23
+ return event
24
+
25
+ if sentry_sdk.get_client().get_integration(ModulesIntegration) is None:
26
+ return event
27
+
28
+ event["modules"] = _get_installed_modules()
46
29
  return event