golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__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.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +209 -87
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +81 -53
  12. golf/commands/run.py +30 -15
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +493 -362
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +216 -95
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +455 -310
  42. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.13.dist-info/RECORD +55 -0
  44. golf_mcp-0.1.11.dist-info/RECORD +0 -55
  45. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
  46. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
@@ -1,43 +1,46 @@
1
1
  """Component-level OpenTelemetry instrumentation for Golf-built servers."""
2
2
 
3
+ import asyncio
4
+ import functools
3
5
  import os
4
6
  import sys
5
- import functools
6
- from typing import Callable, Optional, TypeVar
7
+ from collections.abc import Callable
7
8
  from contextlib import asynccontextmanager
8
- import asyncio
9
+ from typing import TypeVar
9
10
 
10
- from opentelemetry import trace
11
+ from opentelemetry import baggage, trace
12
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
13
+ from opentelemetry.sdk.resources import Resource
11
14
  from opentelemetry.sdk.trace import TracerProvider
12
15
  from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
13
- from opentelemetry.sdk.resources import Resource
14
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
15
- from opentelemetry.trace import Status, StatusCode, Span
16
- from opentelemetry import baggage
16
+ from opentelemetry.trace import Status, StatusCode
17
17
 
18
- T = TypeVar('T')
18
+ T = TypeVar("T")
19
19
 
20
20
  # Global tracer instance
21
- _tracer: Optional[trace.Tracer] = None
22
- _provider: Optional[TracerProvider] = None
21
+ _tracer: trace.Tracer | None = None
22
+ _provider: TracerProvider | None = None
23
+
23
24
 
24
- def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProvider]:
25
+ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | None:
25
26
  """Initialize OpenTelemetry with environment-based configuration.
26
-
27
+
27
28
  Returns None if required environment variables are not set.
28
29
  """
29
30
  global _provider
30
-
31
+
31
32
  # Check for required environment variables based on exporter type
32
33
  exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
33
-
34
+
34
35
  # For OTLP HTTP exporter, check if endpoint is configured
35
36
  if exporter_type == "otlp_http":
36
37
  endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
37
38
  if not endpoint:
38
- print(f"[WARNING] OpenTelemetry tracing is disabled: OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter")
39
+ print(
40
+ "[WARNING] OpenTelemetry tracing is disabled: OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter"
41
+ )
39
42
  return None
40
-
43
+
41
44
  # Create resource with service information
42
45
  resource_attributes = {
43
46
  "service.name": os.environ.get("OTEL_SERVICE_NAME", service_name),
@@ -45,16 +48,18 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
45
48
  "service.instance.id": os.environ.get("SERVICE_INSTANCE_ID", "default"),
46
49
  }
47
50
  resource = Resource.create(resource_attributes)
48
-
51
+
49
52
  # Create provider
50
53
  provider = TracerProvider(resource=resource)
51
-
54
+
52
55
  # Configure exporter based on type
53
56
  try:
54
57
  if exporter_type == "otlp_http":
55
- endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
58
+ endpoint = os.environ.get(
59
+ "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
60
+ )
56
61
  headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
57
-
62
+
58
63
  # Parse headers if provided
59
64
  header_dict = {}
60
65
  if headers:
@@ -62,19 +67,19 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
62
67
  if "=" in header:
63
68
  key, value = header.split("=", 1)
64
69
  header_dict[key.strip()] = value.strip()
65
-
70
+
66
71
  exporter = OTLPSpanExporter(
67
- endpoint=endpoint,
68
- headers=header_dict if header_dict else None
72
+ endpoint=endpoint, headers=header_dict if header_dict else None
69
73
  )
70
74
  else:
71
75
  # Default to console exporter
72
76
  exporter = ConsoleSpanExporter(out=sys.stderr)
73
- except Exception as e:
77
+ except Exception:
74
78
  import traceback
79
+
75
80
  traceback.print_exc()
76
81
  raise
77
-
82
+
78
83
  # Add batch processor for better performance
79
84
  try:
80
85
  processor = BatchSpanProcessor(
@@ -82,27 +87,32 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
82
87
  max_queue_size=2048,
83
88
  schedule_delay_millis=1000, # Export every 1 second instead of default 5 seconds
84
89
  max_export_batch_size=512,
85
- export_timeout_millis=5000
90
+ export_timeout_millis=5000,
86
91
  )
87
92
  provider.add_span_processor(processor)
88
- except Exception as e:
93
+ except Exception:
89
94
  import traceback
95
+
90
96
  traceback.print_exc()
91
97
  raise
92
-
98
+
93
99
  # Set as global provider
94
100
  try:
95
101
  # Check if a provider is already set to avoid the warning
96
102
  existing_provider = trace.get_tracer_provider()
97
- if existing_provider is None or str(type(existing_provider).__name__) == 'ProxyTracerProvider':
103
+ if (
104
+ existing_provider is None
105
+ or str(type(existing_provider).__name__) == "ProxyTracerProvider"
106
+ ):
98
107
  # Only set if no provider exists or it's the default proxy provider
99
108
  trace.set_tracer_provider(provider)
100
109
  _provider = provider
101
- except Exception as e:
110
+ except Exception:
102
111
  import traceback
112
+
103
113
  traceback.print_exc()
104
114
  raise
105
-
115
+
106
116
  # Create a test span to verify everything is working
107
117
  try:
108
118
  test_tracer = provider.get_tracer("golf.telemetry.test", "1.0.0")
@@ -110,45 +120,52 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
110
120
  span.set_attribute("test", True)
111
121
  span.set_attribute("service.name", service_name)
112
122
  span.set_attribute("exporter.type", exporter_type)
113
- span.set_attribute("endpoint", os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "not set"))
114
- except Exception as e:
123
+ span.set_attribute(
124
+ "endpoint", os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "not set")
125
+ )
126
+ except Exception:
115
127
  import traceback
128
+
116
129
  traceback.print_exc()
117
-
130
+
118
131
  return provider
119
132
 
133
+
120
134
  def get_tracer() -> trace.Tracer:
121
135
  """Get or create the global tracer instance."""
122
136
  global _tracer, _provider
123
-
137
+
124
138
  # If no provider is set, telemetry is disabled - return no-op tracer
125
139
  if _provider is None:
126
140
  return trace.get_tracer("golf.mcp.components.noop", "1.0.0")
127
-
141
+
128
142
  if _tracer is None:
129
143
  _tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
130
144
  return _tracer
131
145
 
146
+
132
147
  def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
133
148
  """Instrument a tool function with OpenTelemetry tracing."""
134
149
  global _provider
135
-
150
+
136
151
  # If telemetry is disabled, return the original function
137
152
  if _provider is None:
138
153
  return func
139
-
154
+
140
155
  tracer = get_tracer()
141
-
156
+
142
157
  # Add debug logging
143
- print(f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})")
144
-
158
+ print(
159
+ f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})"
160
+ )
161
+
145
162
  @functools.wraps(func)
146
163
  async def async_wrapper(*args, **kwargs):
147
164
  print(f"[TELEMETRY DEBUG] Executing async tool: {tool_name}")
148
-
165
+
149
166
  # Create a more descriptive span name
150
167
  span_name = f"mcp.tool.{tool_name}.execute"
151
-
168
+
152
169
  # start_as_current_span automatically uses the current context and manages it
153
170
  with tracer.start_as_current_span(span_name) as span:
154
171
  # Add comprehensive attributes
@@ -156,45 +173,54 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
156
173
  span.set_attribute("mcp.component.name", tool_name)
157
174
  span.set_attribute("mcp.tool.name", tool_name)
158
175
  span.set_attribute("mcp.tool.function", func.__name__)
159
- span.set_attribute("mcp.tool.module", func.__module__ if hasattr(func, '__module__') else "unknown")
160
-
176
+ span.set_attribute(
177
+ "mcp.tool.module",
178
+ func.__module__ if hasattr(func, "__module__") else "unknown",
179
+ )
180
+
161
181
  # Add execution context
162
182
  span.set_attribute("mcp.execution.args_count", len(args))
163
183
  span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
164
184
  span.set_attribute("mcp.execution.async", True)
165
-
185
+
166
186
  # Extract Context parameter if present
167
- ctx = kwargs.get('ctx')
187
+ ctx = kwargs.get("ctx")
168
188
  if ctx:
169
189
  # Only extract known MCP context attributes
170
- ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
190
+ ctx_attrs = [
191
+ "request_id",
192
+ "session_id",
193
+ "client_id",
194
+ "user_id",
195
+ "tenant_id",
196
+ ]
171
197
  for attr in ctx_attrs:
172
198
  if hasattr(ctx, attr):
173
199
  value = getattr(ctx, attr)
174
200
  if value is not None:
175
201
  span.set_attribute(f"mcp.context.{attr}", str(value))
176
-
202
+
177
203
  # Also check baggage for session ID
178
204
  session_id_from_baggage = baggage.get_baggage("mcp.session.id")
179
205
  if session_id_from_baggage:
180
206
  span.set_attribute("mcp.session.id", session_id_from_baggage)
181
-
207
+
182
208
  # Add tool arguments as span attributes (be careful with sensitive data)
183
209
  for i, arg in enumerate(args):
184
- if isinstance(arg, (str, int, float, bool)) or arg is None:
210
+ if isinstance(arg, str | int | float | bool) or arg is None:
185
211
  span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
186
- elif hasattr(arg, '__dict__'):
212
+ elif hasattr(arg, "__dict__"):
187
213
  # For objects, just record the type
188
214
  span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
189
-
215
+
190
216
  # Add named arguments with better naming
191
217
  for key, value in kwargs.items():
192
- if key != 'ctx':
218
+ if key != "ctx":
193
219
  if value is None:
194
220
  span.set_attribute(f"mcp.tool.input.{key}", "null")
195
- elif isinstance(value, (str, int, float, bool)):
221
+ elif isinstance(value, str | int | float | bool):
196
222
  span.set_attribute(f"mcp.tool.input.{key}", str(value))
197
- elif isinstance(value, (list, tuple)):
223
+ elif isinstance(value, list | tuple):
198
224
  span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
199
225
  span.set_attribute(f"mcp.tool.input.{key}.type", "array")
200
226
  elif isinstance(value, dict):
@@ -204,33 +230,41 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
204
230
  if len(value) > 0 and len(value) <= 5:
205
231
  keys_list = list(value.keys())[:5]
206
232
  # Limit key length and join
207
- truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
208
- span.set_attribute(f"mcp.tool.input.{key}.sample_keys", ",".join(truncated_keys))
233
+ truncated_keys = [
234
+ str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
235
+ for k in keys_list
236
+ ]
237
+ span.set_attribute(
238
+ f"mcp.tool.input.{key}.sample_keys",
239
+ ",".join(truncated_keys),
240
+ )
209
241
  else:
210
242
  # For other types, at least record the type
211
- span.set_attribute(f"mcp.tool.input.{key}.type", type(value).__name__)
212
-
243
+ span.set_attribute(
244
+ f"mcp.tool.input.{key}.type", type(value).__name__
245
+ )
246
+
213
247
  # Add event for tool execution start
214
- span.add_event("tool.execution.started", {
215
- "tool.name": tool_name
216
- })
217
-
218
- print(f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})")
219
-
248
+ span.add_event("tool.execution.started", {"tool.name": tool_name})
249
+
250
+ print(
251
+ f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
252
+ )
253
+
220
254
  try:
221
255
  result = await func(*args, **kwargs)
222
256
  span.set_status(Status(StatusCode.OK))
223
-
257
+
224
258
  # Add event for successful completion
225
- span.add_event("tool.execution.completed", {
226
- "tool.name": tool_name
227
- })
228
-
259
+ span.add_event("tool.execution.completed", {"tool.name": tool_name})
260
+
229
261
  # Capture result metadata with better structure
230
262
  if result is not None:
231
- if isinstance(result, (str, int, float, bool)):
263
+ if isinstance(result, str | int | float | bool):
232
264
  span.set_attribute("mcp.tool.result.value", str(result))
233
- span.set_attribute("mcp.tool.result.type", type(result).__name__)
265
+ span.set_attribute(
266
+ "mcp.tool.result.type", type(result).__name__
267
+ )
234
268
  elif isinstance(result, list):
235
269
  span.set_attribute("mcp.tool.result.count", len(result))
236
270
  span.set_attribute("mcp.tool.result.type", "array")
@@ -241,36 +275,46 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
241
275
  if len(result) > 0 and len(result) <= 5:
242
276
  keys_list = list(result.keys())[:5]
243
277
  # Limit key length and join
244
- truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
245
- span.set_attribute("mcp.tool.result.sample_keys", ",".join(truncated_keys))
246
- elif hasattr(result, '__len__'):
278
+ truncated_keys = [
279
+ str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
280
+ for k in keys_list
281
+ ]
282
+ span.set_attribute(
283
+ "mcp.tool.result.sample_keys", ",".join(truncated_keys)
284
+ )
285
+ elif hasattr(result, "__len__"):
247
286
  span.set_attribute("mcp.tool.result.length", len(result))
248
-
287
+
249
288
  # For any result, record its type
250
289
  span.set_attribute("mcp.tool.result.class", type(result).__name__)
251
-
252
- print(f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}")
290
+
291
+ print(
292
+ f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
293
+ )
253
294
  return result
254
295
  except Exception as e:
255
296
  span.record_exception(e)
256
297
  span.set_status(Status(StatusCode.ERROR, str(e)))
257
-
298
+
258
299
  # Add event for error
259
- span.add_event("tool.execution.error", {
260
- "tool.name": tool_name,
261
- "error.type": type(e).__name__,
262
- "error.message": str(e)
263
- })
300
+ span.add_event(
301
+ "tool.execution.error",
302
+ {
303
+ "tool.name": tool_name,
304
+ "error.type": type(e).__name__,
305
+ "error.message": str(e),
306
+ },
307
+ )
264
308
  print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
265
309
  raise
266
-
310
+
267
311
  @functools.wraps(func)
268
312
  def sync_wrapper(*args, **kwargs):
269
313
  print(f"[TELEMETRY DEBUG] Executing sync tool: {tool_name}")
270
-
314
+
271
315
  # Create a more descriptive span name
272
316
  span_name = f"mcp.tool.{tool_name}.execute"
273
-
317
+
274
318
  # start_as_current_span automatically uses the current context and manages it
275
319
  with tracer.start_as_current_span(span_name) as span:
276
320
  # Add comprehensive attributes
@@ -278,45 +322,54 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
278
322
  span.set_attribute("mcp.component.name", tool_name)
279
323
  span.set_attribute("mcp.tool.name", tool_name)
280
324
  span.set_attribute("mcp.tool.function", func.__name__)
281
- span.set_attribute("mcp.tool.module", func.__module__ if hasattr(func, '__module__') else "unknown")
282
-
325
+ span.set_attribute(
326
+ "mcp.tool.module",
327
+ func.__module__ if hasattr(func, "__module__") else "unknown",
328
+ )
329
+
283
330
  # Add execution context
284
331
  span.set_attribute("mcp.execution.args_count", len(args))
285
332
  span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
286
333
  span.set_attribute("mcp.execution.async", False)
287
-
334
+
288
335
  # Extract Context parameter if present
289
- ctx = kwargs.get('ctx')
336
+ ctx = kwargs.get("ctx")
290
337
  if ctx:
291
338
  # Only extract known MCP context attributes
292
- ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
339
+ ctx_attrs = [
340
+ "request_id",
341
+ "session_id",
342
+ "client_id",
343
+ "user_id",
344
+ "tenant_id",
345
+ ]
293
346
  for attr in ctx_attrs:
294
347
  if hasattr(ctx, attr):
295
348
  value = getattr(ctx, attr)
296
349
  if value is not None:
297
350
  span.set_attribute(f"mcp.context.{attr}", str(value))
298
-
351
+
299
352
  # Also check baggage for session ID
300
353
  session_id_from_baggage = baggage.get_baggage("mcp.session.id")
301
354
  if session_id_from_baggage:
302
355
  span.set_attribute("mcp.session.id", session_id_from_baggage)
303
-
356
+
304
357
  # Add tool arguments as span attributes (be careful with sensitive data)
305
358
  for i, arg in enumerate(args):
306
- if isinstance(arg, (str, int, float, bool)) or arg is None:
359
+ if isinstance(arg, str | int | float | bool) or arg is None:
307
360
  span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
308
- elif hasattr(arg, '__dict__'):
361
+ elif hasattr(arg, "__dict__"):
309
362
  # For objects, just record the type
310
363
  span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
311
-
364
+
312
365
  # Add named arguments with better naming
313
366
  for key, value in kwargs.items():
314
- if key != 'ctx':
367
+ if key != "ctx":
315
368
  if value is None:
316
369
  span.set_attribute(f"mcp.tool.input.{key}", "null")
317
- elif isinstance(value, (str, int, float, bool)):
370
+ elif isinstance(value, str | int | float | bool):
318
371
  span.set_attribute(f"mcp.tool.input.{key}", str(value))
319
- elif isinstance(value, (list, tuple)):
372
+ elif isinstance(value, list | tuple):
320
373
  span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
321
374
  span.set_attribute(f"mcp.tool.input.{key}.type", "array")
322
375
  elif isinstance(value, dict):
@@ -326,33 +379,41 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
326
379
  if len(value) > 0 and len(value) <= 5:
327
380
  keys_list = list(value.keys())[:5]
328
381
  # Limit key length and join
329
- truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
330
- span.set_attribute(f"mcp.tool.input.{key}.sample_keys", ",".join(truncated_keys))
382
+ truncated_keys = [
383
+ str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
384
+ for k in keys_list
385
+ ]
386
+ span.set_attribute(
387
+ f"mcp.tool.input.{key}.sample_keys",
388
+ ",".join(truncated_keys),
389
+ )
331
390
  else:
332
391
  # For other types, at least record the type
333
- span.set_attribute(f"mcp.tool.input.{key}.type", type(value).__name__)
334
-
392
+ span.set_attribute(
393
+ f"mcp.tool.input.{key}.type", type(value).__name__
394
+ )
395
+
335
396
  # Add event for tool execution start
336
- span.add_event("tool.execution.started", {
337
- "tool.name": tool_name
338
- })
339
-
340
- print(f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})")
341
-
397
+ span.add_event("tool.execution.started", {"tool.name": tool_name})
398
+
399
+ print(
400
+ f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
401
+ )
402
+
342
403
  try:
343
404
  result = func(*args, **kwargs)
344
405
  span.set_status(Status(StatusCode.OK))
345
-
406
+
346
407
  # Add event for successful completion
347
- span.add_event("tool.execution.completed", {
348
- "tool.name": tool_name
349
- })
350
-
408
+ span.add_event("tool.execution.completed", {"tool.name": tool_name})
409
+
351
410
  # Capture result metadata with better structure
352
411
  if result is not None:
353
- if isinstance(result, (str, int, float, bool)):
412
+ if isinstance(result, str | int | float | bool):
354
413
  span.set_attribute("mcp.tool.result.value", str(result))
355
- span.set_attribute("mcp.tool.result.type", type(result).__name__)
414
+ span.set_attribute(
415
+ "mcp.tool.result.type", type(result).__name__
416
+ )
356
417
  elif isinstance(result, list):
357
418
  span.set_attribute("mcp.tool.result.count", len(result))
358
419
  span.set_attribute("mcp.tool.result.type", "array")
@@ -363,48 +424,59 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
363
424
  if len(result) > 0 and len(result) <= 5:
364
425
  keys_list = list(result.keys())[:5]
365
426
  # Limit key length and join
366
- truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
367
- span.set_attribute("mcp.tool.result.sample_keys", ",".join(truncated_keys))
368
- elif hasattr(result, '__len__'):
427
+ truncated_keys = [
428
+ str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
429
+ for k in keys_list
430
+ ]
431
+ span.set_attribute(
432
+ "mcp.tool.result.sample_keys", ",".join(truncated_keys)
433
+ )
434
+ elif hasattr(result, "__len__"):
369
435
  span.set_attribute("mcp.tool.result.length", len(result))
370
-
436
+
371
437
  # For any result, record its type
372
438
  span.set_attribute("mcp.tool.result.class", type(result).__name__)
373
-
374
- print(f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}")
439
+
440
+ print(
441
+ f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
442
+ )
375
443
  return result
376
444
  except Exception as e:
377
445
  span.record_exception(e)
378
446
  span.set_status(Status(StatusCode.ERROR, str(e)))
379
-
447
+
380
448
  # Add event for error
381
- span.add_event("tool.execution.error", {
382
- "tool.name": tool_name,
383
- "error.type": type(e).__name__,
384
- "error.message": str(e)
385
- })
449
+ span.add_event(
450
+ "tool.execution.error",
451
+ {
452
+ "tool.name": tool_name,
453
+ "error.type": type(e).__name__,
454
+ "error.message": str(e),
455
+ },
456
+ )
386
457
  print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
387
458
  raise
388
-
459
+
389
460
  # Return appropriate wrapper based on function type
390
461
  if asyncio.iscoroutinefunction(func):
391
462
  return async_wrapper
392
463
  else:
393
464
  return sync_wrapper
394
465
 
466
+
395
467
  def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[..., T]:
396
468
  """Instrument a resource function with OpenTelemetry tracing."""
397
469
  global _provider
398
-
470
+
399
471
  # If telemetry is disabled, return the original function
400
472
  if _provider is None:
401
473
  return func
402
-
474
+
403
475
  tracer = get_tracer()
404
-
476
+
405
477
  # Determine if this is a template based on URI pattern
406
- is_template = '{' in resource_uri
407
-
478
+ is_template = "{" in resource_uri
479
+
408
480
  @functools.wraps(func)
409
481
  async def async_wrapper(*args, **kwargs):
410
482
  # Create a more descriptive span name
@@ -416,43 +488,50 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
416
488
  span.set_attribute("mcp.resource.uri", resource_uri)
417
489
  span.set_attribute("mcp.resource.is_template", is_template)
418
490
  span.set_attribute("mcp.resource.function", func.__name__)
419
- span.set_attribute("mcp.resource.module", func.__module__ if hasattr(func, '__module__') else "unknown")
491
+ span.set_attribute(
492
+ "mcp.resource.module",
493
+ func.__module__ if hasattr(func, "__module__") else "unknown",
494
+ )
420
495
  span.set_attribute("mcp.execution.async", True)
421
-
496
+
422
497
  # Extract Context parameter if present
423
- ctx = kwargs.get('ctx')
498
+ ctx = kwargs.get("ctx")
424
499
  if ctx:
425
500
  # Only extract known MCP context attributes
426
- ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
501
+ ctx_attrs = [
502
+ "request_id",
503
+ "session_id",
504
+ "client_id",
505
+ "user_id",
506
+ "tenant_id",
507
+ ]
427
508
  for attr in ctx_attrs:
428
509
  if hasattr(ctx, attr):
429
510
  value = getattr(ctx, attr)
430
511
  if value is not None:
431
512
  span.set_attribute(f"mcp.context.{attr}", str(value))
432
-
513
+
433
514
  # Also check baggage for session ID
434
515
  session_id_from_baggage = baggage.get_baggage("mcp.session.id")
435
516
  if session_id_from_baggage:
436
517
  span.set_attribute("mcp.session.id", session_id_from_baggage)
437
-
518
+
438
519
  # Add event for resource read start
439
- span.add_event("resource.read.started", {
440
- "resource.uri": resource_uri
441
- })
442
-
520
+ span.add_event("resource.read.started", {"resource.uri": resource_uri})
521
+
443
522
  try:
444
523
  result = await func(*args, **kwargs)
445
524
  span.set_status(Status(StatusCode.OK))
446
-
525
+
447
526
  # Add event for successful read
448
- span.add_event("resource.read.completed", {
449
- "resource.uri": resource_uri
450
- })
451
-
527
+ span.add_event(
528
+ "resource.read.completed", {"resource.uri": resource_uri}
529
+ )
530
+
452
531
  # Add result metadata
453
- if hasattr(result, '__len__'):
532
+ if hasattr(result, "__len__"):
454
533
  span.set_attribute("mcp.resource.result.size", len(result))
455
-
534
+
456
535
  # Determine content type if possible
457
536
  if isinstance(result, str):
458
537
  span.set_attribute("mcp.resource.result.type", "text")
@@ -466,20 +545,23 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
466
545
  elif isinstance(result, list):
467
546
  span.set_attribute("mcp.resource.result.type", "array")
468
547
  span.set_attribute("mcp.resource.result.items_count", len(result))
469
-
548
+
470
549
  return result
471
550
  except Exception as e:
472
551
  span.record_exception(e)
473
552
  span.set_status(Status(StatusCode.ERROR, str(e)))
474
-
553
+
475
554
  # Add event for error
476
- span.add_event("resource.read.error", {
477
- "resource.uri": resource_uri,
478
- "error.type": type(e).__name__,
479
- "error.message": str(e)
480
- })
555
+ span.add_event(
556
+ "resource.read.error",
557
+ {
558
+ "resource.uri": resource_uri,
559
+ "error.type": type(e).__name__,
560
+ "error.message": str(e),
561
+ },
562
+ )
481
563
  raise
482
-
564
+
483
565
  @functools.wraps(func)
484
566
  def sync_wrapper(*args, **kwargs):
485
567
  # Create a more descriptive span name
@@ -491,43 +573,50 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
491
573
  span.set_attribute("mcp.resource.uri", resource_uri)
492
574
  span.set_attribute("mcp.resource.is_template", is_template)
493
575
  span.set_attribute("mcp.resource.function", func.__name__)
494
- span.set_attribute("mcp.resource.module", func.__module__ if hasattr(func, '__module__') else "unknown")
576
+ span.set_attribute(
577
+ "mcp.resource.module",
578
+ func.__module__ if hasattr(func, "__module__") else "unknown",
579
+ )
495
580
  span.set_attribute("mcp.execution.async", False)
496
-
581
+
497
582
  # Extract Context parameter if present
498
- ctx = kwargs.get('ctx')
583
+ ctx = kwargs.get("ctx")
499
584
  if ctx:
500
585
  # Only extract known MCP context attributes
501
- ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
586
+ ctx_attrs = [
587
+ "request_id",
588
+ "session_id",
589
+ "client_id",
590
+ "user_id",
591
+ "tenant_id",
592
+ ]
502
593
  for attr in ctx_attrs:
503
594
  if hasattr(ctx, attr):
504
595
  value = getattr(ctx, attr)
505
596
  if value is not None:
506
597
  span.set_attribute(f"mcp.context.{attr}", str(value))
507
-
598
+
508
599
  # Also check baggage for session ID
509
600
  session_id_from_baggage = baggage.get_baggage("mcp.session.id")
510
601
  if session_id_from_baggage:
511
602
  span.set_attribute("mcp.session.id", session_id_from_baggage)
512
-
603
+
513
604
  # Add event for resource read start
514
- span.add_event("resource.read.started", {
515
- "resource.uri": resource_uri
516
- })
517
-
605
+ span.add_event("resource.read.started", {"resource.uri": resource_uri})
606
+
518
607
  try:
519
608
  result = func(*args, **kwargs)
520
609
  span.set_status(Status(StatusCode.OK))
521
-
610
+
522
611
  # Add event for successful read
523
- span.add_event("resource.read.completed", {
524
- "resource.uri": resource_uri
525
- })
526
-
612
+ span.add_event(
613
+ "resource.read.completed", {"resource.uri": resource_uri}
614
+ )
615
+
527
616
  # Add result metadata
528
- if hasattr(result, '__len__'):
617
+ if hasattr(result, "__len__"):
529
618
  span.set_attribute("mcp.resource.result.size", len(result))
530
-
619
+
531
620
  # Determine content type if possible
532
621
  if isinstance(result, str):
533
622
  span.set_attribute("mcp.resource.result.type", "text")
@@ -541,35 +630,39 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
541
630
  elif isinstance(result, list):
542
631
  span.set_attribute("mcp.resource.result.type", "array")
543
632
  span.set_attribute("mcp.resource.result.items_count", len(result))
544
-
633
+
545
634
  return result
546
635
  except Exception as e:
547
636
  span.record_exception(e)
548
637
  span.set_status(Status(StatusCode.ERROR, str(e)))
549
-
638
+
550
639
  # Add event for error
551
- span.add_event("resource.read.error", {
552
- "resource.uri": resource_uri,
553
- "error.type": type(e).__name__,
554
- "error.message": str(e)
555
- })
640
+ span.add_event(
641
+ "resource.read.error",
642
+ {
643
+ "resource.uri": resource_uri,
644
+ "error.type": type(e).__name__,
645
+ "error.message": str(e),
646
+ },
647
+ )
556
648
  raise
557
-
649
+
558
650
  if asyncio.iscoroutinefunction(func):
559
651
  return async_wrapper
560
652
  else:
561
653
  return sync_wrapper
562
654
 
655
+
563
656
  def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
564
657
  """Instrument a prompt function with OpenTelemetry tracing."""
565
658
  global _provider
566
-
659
+
567
660
  # If telemetry is disabled, return the original function
568
661
  if _provider is None:
569
662
  return func
570
-
663
+
571
664
  tracer = get_tracer()
572
-
665
+
573
666
  @functools.wraps(func)
574
667
  async def async_wrapper(*args, **kwargs):
575
668
  # Create a more descriptive span name
@@ -580,83 +673,100 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
580
673
  span.set_attribute("mcp.component.name", prompt_name)
581
674
  span.set_attribute("mcp.prompt.name", prompt_name)
582
675
  span.set_attribute("mcp.prompt.function", func.__name__)
583
- span.set_attribute("mcp.prompt.module", func.__module__ if hasattr(func, '__module__') else "unknown")
676
+ span.set_attribute(
677
+ "mcp.prompt.module",
678
+ func.__module__ if hasattr(func, "__module__") else "unknown",
679
+ )
584
680
  span.set_attribute("mcp.execution.async", True)
585
-
681
+
586
682
  # Extract Context parameter if present
587
- ctx = kwargs.get('ctx')
683
+ ctx = kwargs.get("ctx")
588
684
  if ctx:
589
685
  # Only extract known MCP context attributes
590
- ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
686
+ ctx_attrs = [
687
+ "request_id",
688
+ "session_id",
689
+ "client_id",
690
+ "user_id",
691
+ "tenant_id",
692
+ ]
591
693
  for attr in ctx_attrs:
592
694
  if hasattr(ctx, attr):
593
695
  value = getattr(ctx, attr)
594
696
  if value is not None:
595
697
  span.set_attribute(f"mcp.context.{attr}", str(value))
596
-
698
+
597
699
  # Also check baggage for session ID
598
700
  session_id_from_baggage = baggage.get_baggage("mcp.session.id")
599
701
  if session_id_from_baggage:
600
702
  span.set_attribute("mcp.session.id", session_id_from_baggage)
601
-
703
+
602
704
  # Add prompt arguments
603
705
  for key, value in kwargs.items():
604
- if key != 'ctx':
605
- if isinstance(value, (str, int, float, bool)) or value is None:
706
+ if key != "ctx":
707
+ if isinstance(value, str | int | float | bool) or value is None:
606
708
  span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
607
709
  else:
608
- span.set_attribute(f"mcp.prompt.arg.{key}.type", type(value).__name__)
609
-
710
+ span.set_attribute(
711
+ f"mcp.prompt.arg.{key}.type", type(value).__name__
712
+ )
713
+
610
714
  # Add event for prompt generation start
611
- span.add_event("prompt.generation.started", {
612
- "prompt.name": prompt_name
613
- })
614
-
715
+ span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
716
+
615
717
  try:
616
718
  result = await func(*args, **kwargs)
617
719
  span.set_status(Status(StatusCode.OK))
618
-
720
+
619
721
  # Add event for successful generation
620
- span.add_event("prompt.generation.completed", {
621
- "prompt.name": prompt_name
622
- })
623
-
722
+ span.add_event(
723
+ "prompt.generation.completed", {"prompt.name": prompt_name}
724
+ )
725
+
624
726
  # Add message count and type information
625
727
  if isinstance(result, list):
626
728
  span.set_attribute("mcp.prompt.result.message_count", len(result))
627
729
  span.set_attribute("mcp.prompt.result.type", "message_list")
628
-
730
+
629
731
  # Analyze message types if they have role attributes
630
732
  roles = []
631
733
  for msg in result:
632
- if hasattr(msg, 'role'):
734
+ if hasattr(msg, "role"):
633
735
  roles.append(msg.role)
634
- elif isinstance(msg, dict) and 'role' in msg:
635
- roles.append(msg['role'])
636
-
736
+ elif isinstance(msg, dict) and "role" in msg:
737
+ roles.append(msg["role"])
738
+
637
739
  if roles:
638
740
  unique_roles = list(set(roles))
639
- span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
640
- span.set_attribute("mcp.prompt.result.role_counts", str({role: roles.count(role) for role in unique_roles}))
741
+ span.set_attribute(
742
+ "mcp.prompt.result.roles", ",".join(unique_roles)
743
+ )
744
+ span.set_attribute(
745
+ "mcp.prompt.result.role_counts",
746
+ str({role: roles.count(role) for role in unique_roles}),
747
+ )
641
748
  elif isinstance(result, str):
642
749
  span.set_attribute("mcp.prompt.result.type", "string")
643
750
  span.set_attribute("mcp.prompt.result.length", len(result))
644
751
  else:
645
752
  span.set_attribute("mcp.prompt.result.type", type(result).__name__)
646
-
753
+
647
754
  return result
648
755
  except Exception as e:
649
756
  span.record_exception(e)
650
757
  span.set_status(Status(StatusCode.ERROR, str(e)))
651
-
758
+
652
759
  # Add event for error
653
- span.add_event("prompt.generation.error", {
654
- "prompt.name": prompt_name,
655
- "error.type": type(e).__name__,
656
- "error.message": str(e)
657
- })
760
+ span.add_event(
761
+ "prompt.generation.error",
762
+ {
763
+ "prompt.name": prompt_name,
764
+ "error.type": type(e).__name__,
765
+ "error.message": str(e),
766
+ },
767
+ )
658
768
  raise
659
-
769
+
660
770
  @functools.wraps(func)
661
771
  def sync_wrapper(*args, **kwargs):
662
772
  # Create a more descriptive span name
@@ -667,119 +777,137 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
667
777
  span.set_attribute("mcp.component.name", prompt_name)
668
778
  span.set_attribute("mcp.prompt.name", prompt_name)
669
779
  span.set_attribute("mcp.prompt.function", func.__name__)
670
- span.set_attribute("mcp.prompt.module", func.__module__ if hasattr(func, '__module__') else "unknown")
780
+ span.set_attribute(
781
+ "mcp.prompt.module",
782
+ func.__module__ if hasattr(func, "__module__") else "unknown",
783
+ )
671
784
  span.set_attribute("mcp.execution.async", False)
672
-
785
+
673
786
  # Extract Context parameter if present
674
- ctx = kwargs.get('ctx')
787
+ ctx = kwargs.get("ctx")
675
788
  if ctx:
676
789
  # Only extract known MCP context attributes
677
- ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
790
+ ctx_attrs = [
791
+ "request_id",
792
+ "session_id",
793
+ "client_id",
794
+ "user_id",
795
+ "tenant_id",
796
+ ]
678
797
  for attr in ctx_attrs:
679
798
  if hasattr(ctx, attr):
680
799
  value = getattr(ctx, attr)
681
800
  if value is not None:
682
801
  span.set_attribute(f"mcp.context.{attr}", str(value))
683
-
802
+
684
803
  # Also check baggage for session ID
685
804
  session_id_from_baggage = baggage.get_baggage("mcp.session.id")
686
805
  if session_id_from_baggage:
687
806
  span.set_attribute("mcp.session.id", session_id_from_baggage)
688
-
807
+
689
808
  # Add prompt arguments
690
809
  for key, value in kwargs.items():
691
- if key != 'ctx':
692
- if isinstance(value, (str, int, float, bool)) or value is None:
810
+ if key != "ctx":
811
+ if isinstance(value, str | int | float | bool) or value is None:
693
812
  span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
694
813
  else:
695
- span.set_attribute(f"mcp.prompt.arg.{key}.type", type(value).__name__)
696
-
814
+ span.set_attribute(
815
+ f"mcp.prompt.arg.{key}.type", type(value).__name__
816
+ )
817
+
697
818
  # Add event for prompt generation start
698
- span.add_event("prompt.generation.started", {
699
- "prompt.name": prompt_name
700
- })
701
-
819
+ span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
820
+
702
821
  try:
703
822
  result = func(*args, **kwargs)
704
823
  span.set_status(Status(StatusCode.OK))
705
-
824
+
706
825
  # Add event for successful generation
707
- span.add_event("prompt.generation.completed", {
708
- "prompt.name": prompt_name
709
- })
710
-
826
+ span.add_event(
827
+ "prompt.generation.completed", {"prompt.name": prompt_name}
828
+ )
829
+
711
830
  # Add message count and type information
712
831
  if isinstance(result, list):
713
832
  span.set_attribute("mcp.prompt.result.message_count", len(result))
714
833
  span.set_attribute("mcp.prompt.result.type", "message_list")
715
-
834
+
716
835
  # Analyze message types if they have role attributes
717
836
  roles = []
718
837
  for msg in result:
719
- if hasattr(msg, 'role'):
838
+ if hasattr(msg, "role"):
720
839
  roles.append(msg.role)
721
- elif isinstance(msg, dict) and 'role' in msg:
722
- roles.append(msg['role'])
723
-
840
+ elif isinstance(msg, dict) and "role" in msg:
841
+ roles.append(msg["role"])
842
+
724
843
  if roles:
725
844
  unique_roles = list(set(roles))
726
- span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
727
- span.set_attribute("mcp.prompt.result.role_counts", str({role: roles.count(role) for role in unique_roles}))
845
+ span.set_attribute(
846
+ "mcp.prompt.result.roles", ",".join(unique_roles)
847
+ )
848
+ span.set_attribute(
849
+ "mcp.prompt.result.role_counts",
850
+ str({role: roles.count(role) for role in unique_roles}),
851
+ )
728
852
  elif isinstance(result, str):
729
853
  span.set_attribute("mcp.prompt.result.type", "string")
730
854
  span.set_attribute("mcp.prompt.result.length", len(result))
731
855
  else:
732
856
  span.set_attribute("mcp.prompt.result.type", type(result).__name__)
733
-
857
+
734
858
  return result
735
859
  except Exception as e:
736
860
  span.record_exception(e)
737
861
  span.set_status(Status(StatusCode.ERROR, str(e)))
738
-
862
+
739
863
  # Add event for error
740
- span.add_event("prompt.generation.error", {
741
- "prompt.name": prompt_name,
742
- "error.type": type(e).__name__,
743
- "error.message": str(e)
744
- })
864
+ span.add_event(
865
+ "prompt.generation.error",
866
+ {
867
+ "prompt.name": prompt_name,
868
+ "error.type": type(e).__name__,
869
+ "error.message": str(e),
870
+ },
871
+ )
745
872
  raise
746
-
873
+
747
874
  if asyncio.iscoroutinefunction(func):
748
875
  return async_wrapper
749
876
  else:
750
877
  return sync_wrapper
751
878
 
879
+
752
880
  @asynccontextmanager
753
881
  async def telemetry_lifespan(mcp_instance):
754
882
  """Simplified lifespan for telemetry initialization and cleanup."""
755
883
  global _provider
756
-
884
+
757
885
  # Initialize telemetry with the server name
758
886
  provider = init_telemetry(service_name=mcp_instance.name)
759
-
887
+
760
888
  # If provider is None, telemetry is disabled
761
889
  if provider is None:
762
890
  # Just yield without any telemetry setup
763
891
  yield
764
892
  return
765
-
893
+
766
894
  # Try to add session tracking middleware if possible
767
895
  try:
768
896
  from starlette.middleware.base import BaseHTTPMiddleware
769
897
  from starlette.requests import Request
770
-
898
+
771
899
  class SessionTracingMiddleware(BaseHTTPMiddleware):
772
900
  async def dispatch(self, request: Request, call_next):
773
901
  # Extract session ID from query params or headers
774
- session_id = request.query_params.get('session_id')
902
+ session_id = request.query_params.get("session_id")
775
903
  if not session_id:
776
904
  # Check headers as fallback
777
- session_id = request.headers.get('x-session-id')
778
-
905
+ session_id = request.headers.get("x-session-id")
906
+
779
907
  # Create a descriptive span name based on the request
780
908
  method = request.method
781
909
  path = request.url.path
782
-
910
+
783
911
  # Determine the operation type from the path
784
912
  operation_type = "unknown"
785
913
  if "/mcp" in path:
@@ -788,9 +916,9 @@ async def telemetry_lifespan(mcp_instance):
788
916
  operation_type = "sse.stream"
789
917
  elif "/auth" in path:
790
918
  operation_type = "auth"
791
-
919
+
792
920
  span_name = f"{operation_type}.{method.lower()}"
793
-
921
+
794
922
  tracer = get_tracer()
795
923
  with tracer.start_as_current_span(span_name) as span:
796
924
  # Add comprehensive HTTP attributes
@@ -799,102 +927,119 @@ async def telemetry_lifespan(mcp_instance):
799
927
  span.set_attribute("http.scheme", request.url.scheme)
800
928
  span.set_attribute("http.host", request.url.hostname or "unknown")
801
929
  span.set_attribute("http.target", path)
802
- span.set_attribute("http.user_agent", request.headers.get("user-agent", "unknown"))
803
-
930
+ span.set_attribute(
931
+ "http.user_agent", request.headers.get("user-agent", "unknown")
932
+ )
933
+
804
934
  # Add session tracking
805
935
  if session_id:
806
936
  span.set_attribute("mcp.session.id", session_id)
807
937
  # Add to baggage for propagation
808
938
  ctx = baggage.set_baggage("mcp.session.id", session_id)
809
939
  from opentelemetry import context
940
+
810
941
  token = context.attach(ctx)
811
942
  else:
812
943
  token = None
813
-
944
+
814
945
  # Add request size if available
815
946
  content_length = request.headers.get("content-length")
816
947
  if content_length:
817
948
  span.set_attribute("http.request.size", int(content_length))
818
-
949
+
819
950
  # Add event for request start
820
- span.add_event("http.request.started", {
821
- "method": method,
822
- "path": path
823
- })
824
-
951
+ span.add_event(
952
+ "http.request.started", {"method": method, "path": path}
953
+ )
954
+
825
955
  try:
826
956
  response = await call_next(request)
827
-
957
+
828
958
  # Add response attributes
829
959
  span.set_attribute("http.status_code", response.status_code)
830
- span.set_attribute("http.status_class", f"{response.status_code // 100}xx")
831
-
960
+ span.set_attribute(
961
+ "http.status_class", f"{response.status_code // 100}xx"
962
+ )
963
+
832
964
  # Set span status based on HTTP status
833
965
  if response.status_code >= 400:
834
- span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
966
+ span.set_status(
967
+ Status(StatusCode.ERROR, f"HTTP {response.status_code}")
968
+ )
835
969
  else:
836
970
  span.set_status(Status(StatusCode.OK))
837
-
971
+
838
972
  # Add event for request completion
839
- span.add_event("http.request.completed", {
840
- "method": method,
841
- "path": path,
842
- "status_code": response.status_code
843
- })
844
-
973
+ span.add_event(
974
+ "http.request.completed",
975
+ {
976
+ "method": method,
977
+ "path": path,
978
+ "status_code": response.status_code,
979
+ },
980
+ )
981
+
845
982
  return response
846
983
  except Exception as e:
847
984
  span.record_exception(e)
848
985
  span.set_status(Status(StatusCode.ERROR, str(e)))
849
-
986
+
850
987
  # Add event for error
851
- span.add_event("http.request.error", {
852
- "method": method,
853
- "path": path,
854
- "error.type": type(e).__name__,
855
- "error.message": str(e)
856
- })
988
+ span.add_event(
989
+ "http.request.error",
990
+ {
991
+ "method": method,
992
+ "path": path,
993
+ "error.type": type(e).__name__,
994
+ "error.message": str(e),
995
+ },
996
+ )
857
997
  raise
858
998
  finally:
859
999
  if token:
860
1000
  context.detach(token)
861
-
1001
+
862
1002
  # Try to add middleware to FastMCP app if it has Starlette app
863
- if hasattr(mcp_instance, 'app') or hasattr(mcp_instance, '_app'):
864
- app = getattr(mcp_instance, 'app', getattr(mcp_instance, '_app', None))
865
- if app and hasattr(app, 'add_middleware'):
1003
+ if hasattr(mcp_instance, "app") or hasattr(mcp_instance, "_app"):
1004
+ app = getattr(mcp_instance, "app", getattr(mcp_instance, "_app", None))
1005
+ if app and hasattr(app, "add_middleware"):
866
1006
  app.add_middleware(SessionTracingMiddleware)
867
1007
  print("[TELEMETRY DEBUG] Added SessionTracingMiddleware to FastMCP app")
868
-
1008
+
869
1009
  # Also try to instrument FastMCP's internal handlers
870
- if hasattr(mcp_instance, '_tool_manager') and hasattr(mcp_instance._tool_manager, 'tools'):
871
- print(f"[TELEMETRY DEBUG] Found {len(mcp_instance._tool_manager.tools)} tools in FastMCP")
1010
+ if hasattr(mcp_instance, "_tool_manager") and hasattr(
1011
+ mcp_instance._tool_manager, "tools"
1012
+ ):
1013
+ print(
1014
+ f"[TELEMETRY DEBUG] Found {len(mcp_instance._tool_manager.tools)} tools in FastMCP"
1015
+ )
872
1016
  # The tools should already be instrumented when they were registered
873
-
1017
+
874
1018
  # Try to patch FastMCP's request handling to ensure context propagation
875
- if hasattr(mcp_instance, 'handle_request'):
1019
+ if hasattr(mcp_instance, "handle_request"):
876
1020
  original_handle_request = mcp_instance.handle_request
877
-
1021
+
878
1022
  async def traced_handle_request(*args, **kwargs):
879
1023
  tracer = get_tracer()
880
1024
  with tracer.start_as_current_span("mcp.handle_request") as span:
881
1025
  span.set_attribute("mcp.request.handler", "handle_request")
882
1026
  return await original_handle_request(*args, **kwargs)
883
-
1027
+
884
1028
  mcp_instance.handle_request = traced_handle_request
885
1029
  print("[TELEMETRY DEBUG] Patched FastMCP handle_request method")
886
-
1030
+
887
1031
  except Exception as e:
888
1032
  print(f"[TELEMETRY DEBUG] Error setting up telemetry middleware: {e}")
889
1033
  import traceback
1034
+
890
1035
  traceback.print_exc()
891
-
1036
+
892
1037
  try:
893
1038
  # Yield control back to FastMCP
894
1039
  yield
895
1040
  finally:
896
1041
  # Cleanup - shutdown the provider
897
- if _provider and hasattr(_provider, 'shutdown'):
1042
+ if _provider and hasattr(_provider, "shutdown"):
898
1043
  _provider.force_flush()
899
1044
  _provider.shutdown()
900
- _provider = None
1045
+ _provider = None