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

@@ -20,7 +20,6 @@ T = TypeVar('T')
20
20
  # Global tracer instance
21
21
  _tracer: Optional[trace.Tracer] = None
22
22
  _provider: Optional[TracerProvider] = None
23
- _instrumented_tools = []
24
23
 
25
24
  def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProvider]:
26
25
  """Initialize OpenTelemetry with environment-based configuration.
@@ -130,16 +129,6 @@ def get_tracer() -> trace.Tracer:
130
129
  _tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
131
130
  return _tracer
132
131
 
133
- def _add_component_attributes(span: Span, component_type: str, component_name: str, **kwargs):
134
- """Add standard component attributes to a span."""
135
- span.set_attribute("mcp.component.type", component_type)
136
- span.set_attribute("mcp.component.name", component_name)
137
-
138
- # Add any additional attributes
139
- for key, value in kwargs.items():
140
- if value is not None:
141
- span.set_attribute(f"mcp.component.{key}", str(value))
142
-
143
132
  def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
144
133
  """Instrument a tool function with OpenTelemetry tracing."""
145
134
  global _provider
@@ -150,31 +139,39 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
150
139
 
151
140
  tracer = get_tracer()
152
141
 
142
+ # Add debug logging
143
+ print(f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})")
144
+
153
145
  @functools.wraps(func)
154
146
  async def async_wrapper(*args, **kwargs):
155
- span = tracer.start_span(f"tool.{tool_name}")
147
+ print(f"[TELEMETRY DEBUG] Executing async tool: {tool_name}")
156
148
 
157
- # Activate the span in the current context
158
- from opentelemetry import context
159
- token = context.attach(trace.set_span_in_context(span))
149
+ # Create a more descriptive span name
150
+ span_name = f"mcp.tool.{tool_name}.execute"
160
151
 
161
- try:
162
- _add_component_attributes(span, "tool", tool_name,
163
- args_count=len(args),
164
- kwargs_count=len(kwargs))
152
+ # start_as_current_span automatically uses the current context and manages it
153
+ with tracer.start_as_current_span(span_name) as span:
154
+ # Add comprehensive attributes
155
+ span.set_attribute("mcp.component.type", "tool")
156
+ span.set_attribute("mcp.component.name", tool_name)
157
+ span.set_attribute("mcp.tool.name", tool_name)
158
+ span.set_attribute("mcp.tool.function", func.__name__)
159
+ span.set_attribute("mcp.tool.module", func.__module__ if hasattr(func, '__module__') else "unknown")
165
160
 
166
- # Extract Context parameter if present - this should have MCP session info
161
+ # Add execution context
162
+ span.set_attribute("mcp.execution.args_count", len(args))
163
+ span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
164
+ span.set_attribute("mcp.execution.async", True)
165
+
166
+ # Extract Context parameter if present
167
167
  ctx = kwargs.get('ctx')
168
168
  if ctx:
169
- if hasattr(ctx, 'request_id'):
170
- span.set_attribute("mcp.request.id", ctx.request_id)
171
- if hasattr(ctx, 'session_id'):
172
- span.set_attribute("mcp.session.id", ctx.session_id)
173
- # Try to find any session-related attributes
174
- for attr in dir(ctx):
175
- if 'session' in attr.lower() and not attr.startswith('_'):
176
- value = getattr(ctx, attr, None)
177
- if value:
169
+ # Only extract known MCP context attributes
170
+ ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
171
+ for attr in ctx_attrs:
172
+ if hasattr(ctx, attr):
173
+ value = getattr(ctx, attr)
174
+ if value is not None:
178
175
  span.set_attribute(f"mcp.context.{attr}", str(value))
179
176
 
180
177
  # Also check baggage for session ID
@@ -185,89 +182,118 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
185
182
  # Add tool arguments as span attributes (be careful with sensitive data)
186
183
  for i, arg in enumerate(args):
187
184
  if isinstance(arg, (str, int, float, bool)) or arg is None:
188
- span.set_attribute(f"tool.arg.{i}", str(arg))
185
+ span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
189
186
  elif hasattr(arg, '__dict__'):
190
187
  # For objects, just record the type
191
- span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
188
+ span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
192
189
 
193
- # Add named arguments
190
+ # Add named arguments with better naming
194
191
  for key, value in kwargs.items():
195
192
  if key != 'ctx':
196
193
  if value is None:
197
- span.set_attribute(f"tool.kwarg.{key}", "null")
194
+ span.set_attribute(f"mcp.tool.input.{key}", "null")
198
195
  elif isinstance(value, (str, int, float, bool)):
199
- span.set_attribute(f"tool.kwarg.{key}", str(value))
196
+ span.set_attribute(f"mcp.tool.input.{key}", str(value))
200
197
  elif isinstance(value, (list, tuple)):
201
- span.set_attribute(f"tool.kwarg.{key}", f"[{len(value)} items]")
198
+ span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
199
+ span.set_attribute(f"mcp.tool.input.{key}.type", "array")
202
200
  elif isinstance(value, dict):
203
- span.set_attribute(f"tool.kwarg.{key}", f"{{dict with {len(value)} keys}}")
201
+ span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
202
+ span.set_attribute(f"mcp.tool.input.{key}.type", "object")
203
+ # Only show first few keys to avoid exceeding attribute limits
204
+ if len(value) > 0 and len(value) <= 5:
205
+ keys_list = list(value.keys())[:5]
206
+ # 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))
204
209
  else:
205
210
  # For other types, at least record the type
206
- span.set_attribute(f"tool.kwarg.{key}.type", type(value).__name__)
211
+ span.set_attribute(f"mcp.tool.input.{key}.type", type(value).__name__)
212
+
213
+ # 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})")
207
219
 
208
220
  try:
209
221
  result = await func(*args, **kwargs)
210
222
  span.set_status(Status(StatusCode.OK))
211
223
 
212
- # Capture result metadata
224
+ # Add event for successful completion
225
+ span.add_event("tool.execution.completed", {
226
+ "tool.name": tool_name
227
+ })
228
+
229
+ # Capture result metadata with better structure
213
230
  if result is not None:
214
231
  if isinstance(result, (str, int, float, bool)):
215
- span.set_attribute("tool.result", str(result))
232
+ span.set_attribute("mcp.tool.result.value", str(result))
233
+ span.set_attribute("mcp.tool.result.type", type(result).__name__)
216
234
  elif isinstance(result, list):
217
- span.set_attribute("tool.result.count", len(result))
218
- span.set_attribute("tool.result.type", "list")
235
+ span.set_attribute("mcp.tool.result.count", len(result))
236
+ span.set_attribute("mcp.tool.result.type", "array")
219
237
  elif isinstance(result, dict):
220
- span.set_attribute("tool.result.keys", ",".join(result.keys()) if len(result) < 10 else f"{len(result)} keys")
221
- span.set_attribute("tool.result.type", "dict")
238
+ span.set_attribute("mcp.tool.result.count", len(result))
239
+ span.set_attribute("mcp.tool.result.type", "object")
240
+ # Only show first few keys to avoid exceeding attribute limits
241
+ if len(result) > 0 and len(result) <= 5:
242
+ keys_list = list(result.keys())[:5]
243
+ # 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))
222
246
  elif hasattr(result, '__len__'):
223
- span.set_attribute("tool.result.length", len(result))
247
+ span.set_attribute("mcp.tool.result.length", len(result))
224
248
 
225
249
  # For any result, record its type
226
- span.set_attribute("tool.result.class", type(result).__name__)
250
+ span.set_attribute("mcp.tool.result.class", type(result).__name__)
227
251
 
252
+ print(f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}")
228
253
  return result
229
254
  except Exception as e:
230
255
  span.record_exception(e)
231
256
  span.set_status(Status(StatusCode.ERROR, str(e)))
257
+
258
+ # 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
+ })
264
+ print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
232
265
  raise
233
- finally:
234
- # End the span and detach context
235
- span.end()
236
- context.detach(token)
237
-
238
- # Force flush the provider to ensure spans are exported
239
- global _provider
240
- if _provider:
241
- try:
242
- _provider.force_flush(timeout_millis=1000)
243
- except Exception as e:
244
- pass
245
266
 
246
267
  @functools.wraps(func)
247
268
  def sync_wrapper(*args, **kwargs):
248
- span = tracer.start_span(f"tool.{tool_name}")
269
+ print(f"[TELEMETRY DEBUG] Executing sync tool: {tool_name}")
249
270
 
250
- # Activate the span in the current context
251
- from opentelemetry import context
252
- token = context.attach(trace.set_span_in_context(span))
271
+ # Create a more descriptive span name
272
+ span_name = f"mcp.tool.{tool_name}.execute"
253
273
 
254
- try:
255
- _add_component_attributes(span, "tool", tool_name,
256
- args_count=len(args),
257
- kwargs_count=len(kwargs))
274
+ # start_as_current_span automatically uses the current context and manages it
275
+ with tracer.start_as_current_span(span_name) as span:
276
+ # Add comprehensive attributes
277
+ span.set_attribute("mcp.component.type", "tool")
278
+ span.set_attribute("mcp.component.name", tool_name)
279
+ span.set_attribute("mcp.tool.name", tool_name)
280
+ span.set_attribute("mcp.tool.function", func.__name__)
281
+ span.set_attribute("mcp.tool.module", func.__module__ if hasattr(func, '__module__') else "unknown")
282
+
283
+ # Add execution context
284
+ span.set_attribute("mcp.execution.args_count", len(args))
285
+ span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
286
+ span.set_attribute("mcp.execution.async", False)
258
287
 
259
- # Extract Context parameter if present - this should have MCP session info
288
+ # Extract Context parameter if present
260
289
  ctx = kwargs.get('ctx')
261
290
  if ctx:
262
- if hasattr(ctx, 'request_id'):
263
- span.set_attribute("mcp.request.id", ctx.request_id)
264
- if hasattr(ctx, 'session_id'):
265
- span.set_attribute("mcp.session.id", ctx.session_id)
266
- # Try to find any session-related attributes
267
- for attr in dir(ctx):
268
- if 'session' in attr.lower() and not attr.startswith('_'):
269
- value = getattr(ctx, attr, None)
270
- if value:
291
+ # Only extract known MCP context attributes
292
+ ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
293
+ for attr in ctx_attrs:
294
+ if hasattr(ctx, attr):
295
+ value = getattr(ctx, attr)
296
+ if value is not None:
271
297
  span.set_attribute(f"mcp.context.{attr}", str(value))
272
298
 
273
299
  # Also check baggage for session ID
@@ -278,63 +304,87 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
278
304
  # Add tool arguments as span attributes (be careful with sensitive data)
279
305
  for i, arg in enumerate(args):
280
306
  if isinstance(arg, (str, int, float, bool)) or arg is None:
281
- span.set_attribute(f"tool.arg.{i}", str(arg))
307
+ span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
282
308
  elif hasattr(arg, '__dict__'):
283
309
  # For objects, just record the type
284
- span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
310
+ span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
285
311
 
286
- # Add named arguments
312
+ # Add named arguments with better naming
287
313
  for key, value in kwargs.items():
288
314
  if key != 'ctx':
289
315
  if value is None:
290
- span.set_attribute(f"tool.kwarg.{key}", "null")
316
+ span.set_attribute(f"mcp.tool.input.{key}", "null")
291
317
  elif isinstance(value, (str, int, float, bool)):
292
- span.set_attribute(f"tool.kwarg.{key}", str(value))
318
+ span.set_attribute(f"mcp.tool.input.{key}", str(value))
293
319
  elif isinstance(value, (list, tuple)):
294
- span.set_attribute(f"tool.kwarg.{key}", f"[{len(value)} items]")
320
+ span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
321
+ span.set_attribute(f"mcp.tool.input.{key}.type", "array")
295
322
  elif isinstance(value, dict):
296
- span.set_attribute(f"tool.kwarg.{key}", f"{{dict with {len(value)} keys}}")
323
+ span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
324
+ span.set_attribute(f"mcp.tool.input.{key}.type", "object")
325
+ # Only show first few keys to avoid exceeding attribute limits
326
+ if len(value) > 0 and len(value) <= 5:
327
+ keys_list = list(value.keys())[:5]
328
+ # 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))
297
331
  else:
298
332
  # For other types, at least record the type
299
- span.set_attribute(f"tool.kwarg.{key}.type", type(value).__name__)
333
+ span.set_attribute(f"mcp.tool.input.{key}.type", type(value).__name__)
334
+
335
+ # 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})")
300
341
 
301
342
  try:
302
343
  result = func(*args, **kwargs)
303
344
  span.set_status(Status(StatusCode.OK))
304
345
 
305
- # Capture result metadata
346
+ # Add event for successful completion
347
+ span.add_event("tool.execution.completed", {
348
+ "tool.name": tool_name
349
+ })
350
+
351
+ # Capture result metadata with better structure
306
352
  if result is not None:
307
353
  if isinstance(result, (str, int, float, bool)):
308
- span.set_attribute("tool.result", str(result))
354
+ span.set_attribute("mcp.tool.result.value", str(result))
355
+ span.set_attribute("mcp.tool.result.type", type(result).__name__)
309
356
  elif isinstance(result, list):
310
- span.set_attribute("tool.result.count", len(result))
311
- span.set_attribute("tool.result.type", "list")
357
+ span.set_attribute("mcp.tool.result.count", len(result))
358
+ span.set_attribute("mcp.tool.result.type", "array")
312
359
  elif isinstance(result, dict):
313
- span.set_attribute("tool.result.keys", ",".join(result.keys()) if len(result) < 10 else f"{len(result)} keys")
314
- span.set_attribute("tool.result.type", "dict")
360
+ span.set_attribute("mcp.tool.result.count", len(result))
361
+ span.set_attribute("mcp.tool.result.type", "object")
362
+ # Only show first few keys to avoid exceeding attribute limits
363
+ if len(result) > 0 and len(result) <= 5:
364
+ keys_list = list(result.keys())[:5]
365
+ # 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))
315
368
  elif hasattr(result, '__len__'):
316
- span.set_attribute("tool.result.length", len(result))
369
+ span.set_attribute("mcp.tool.result.length", len(result))
317
370
 
318
371
  # For any result, record its type
319
- span.set_attribute("tool.result.class", type(result).__name__)
372
+ span.set_attribute("mcp.tool.result.class", type(result).__name__)
320
373
 
374
+ print(f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}")
321
375
  return result
322
376
  except Exception as e:
323
377
  span.record_exception(e)
324
378
  span.set_status(Status(StatusCode.ERROR, str(e)))
379
+
380
+ # 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
+ })
386
+ print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
325
387
  raise
326
- finally:
327
- # End the span and detach context
328
- span.end()
329
- context.detach(token)
330
-
331
- # Force flush the provider to ensure spans are exported
332
- global _provider
333
- if _provider:
334
- try:
335
- _provider.force_flush(timeout_millis=1000)
336
- except Exception as e:
337
- pass
338
388
 
339
389
  # Return appropriate wrapper based on function type
340
390
  if asyncio.iscoroutinefunction(func):
@@ -357,54 +407,152 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
357
407
 
358
408
  @functools.wraps(func)
359
409
  async def async_wrapper(*args, **kwargs):
360
- span_name = "resource.template.read" if is_template else "resource.read"
410
+ # Create a more descriptive span name
411
+ span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
361
412
  with tracer.start_as_current_span(span_name) as span:
362
- _add_component_attributes(span, "resource", resource_uri,
363
- is_template=is_template)
413
+ # Add comprehensive attributes
414
+ span.set_attribute("mcp.component.type", "resource")
415
+ span.set_attribute("mcp.component.name", resource_uri)
416
+ span.set_attribute("mcp.resource.uri", resource_uri)
417
+ span.set_attribute("mcp.resource.is_template", is_template)
418
+ span.set_attribute("mcp.resource.function", func.__name__)
419
+ span.set_attribute("mcp.resource.module", func.__module__ if hasattr(func, '__module__') else "unknown")
420
+ span.set_attribute("mcp.execution.async", True)
364
421
 
365
422
  # Extract Context parameter if present
366
423
  ctx = kwargs.get('ctx')
367
- if ctx and hasattr(ctx, 'request_id'):
368
- span.set_attribute("mcp.request.id", ctx.request_id)
424
+ if ctx:
425
+ # Only extract known MCP context attributes
426
+ ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
427
+ for attr in ctx_attrs:
428
+ if hasattr(ctx, attr):
429
+ value = getattr(ctx, attr)
430
+ if value is not None:
431
+ span.set_attribute(f"mcp.context.{attr}", str(value))
432
+
433
+ # Also check baggage for session ID
434
+ session_id_from_baggage = baggage.get_baggage("mcp.session.id")
435
+ if session_id_from_baggage:
436
+ span.set_attribute("mcp.session.id", session_id_from_baggage)
437
+
438
+ # Add event for resource read start
439
+ span.add_event("resource.read.started", {
440
+ "resource.uri": resource_uri
441
+ })
369
442
 
370
443
  try:
371
444
  result = await func(*args, **kwargs)
372
445
  span.set_status(Status(StatusCode.OK))
373
446
 
374
- # Add result size if applicable
447
+ # Add event for successful read
448
+ span.add_event("resource.read.completed", {
449
+ "resource.uri": resource_uri
450
+ })
451
+
452
+ # Add result metadata
375
453
  if hasattr(result, '__len__'):
376
- span.set_attribute("mcp.resource.size", len(result))
454
+ span.set_attribute("mcp.resource.result.size", len(result))
455
+
456
+ # Determine content type if possible
457
+ if isinstance(result, str):
458
+ span.set_attribute("mcp.resource.result.type", "text")
459
+ span.set_attribute("mcp.resource.result.length", len(result))
460
+ elif isinstance(result, bytes):
461
+ span.set_attribute("mcp.resource.result.type", "binary")
462
+ span.set_attribute("mcp.resource.result.size_bytes", len(result))
463
+ elif isinstance(result, dict):
464
+ span.set_attribute("mcp.resource.result.type", "object")
465
+ span.set_attribute("mcp.resource.result.keys_count", len(result))
466
+ elif isinstance(result, list):
467
+ span.set_attribute("mcp.resource.result.type", "array")
468
+ span.set_attribute("mcp.resource.result.items_count", len(result))
377
469
 
378
470
  return result
379
471
  except Exception as e:
380
472
  span.record_exception(e)
381
473
  span.set_status(Status(StatusCode.ERROR, str(e)))
474
+
475
+ # 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
+ })
382
481
  raise
383
482
 
384
483
  @functools.wraps(func)
385
484
  def sync_wrapper(*args, **kwargs):
386
- span_name = "resource.template.read" if is_template else "resource.read"
485
+ # Create a more descriptive span name
486
+ span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
387
487
  with tracer.start_as_current_span(span_name) as span:
388
- _add_component_attributes(span, "resource", resource_uri,
389
- is_template=is_template)
488
+ # Add comprehensive attributes
489
+ span.set_attribute("mcp.component.type", "resource")
490
+ span.set_attribute("mcp.component.name", resource_uri)
491
+ span.set_attribute("mcp.resource.uri", resource_uri)
492
+ span.set_attribute("mcp.resource.is_template", is_template)
493
+ span.set_attribute("mcp.resource.function", func.__name__)
494
+ span.set_attribute("mcp.resource.module", func.__module__ if hasattr(func, '__module__') else "unknown")
495
+ span.set_attribute("mcp.execution.async", False)
390
496
 
391
497
  # Extract Context parameter if present
392
498
  ctx = kwargs.get('ctx')
393
- if ctx and hasattr(ctx, 'request_id'):
394
- span.set_attribute("mcp.request.id", ctx.request_id)
499
+ if ctx:
500
+ # Only extract known MCP context attributes
501
+ ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
502
+ for attr in ctx_attrs:
503
+ if hasattr(ctx, attr):
504
+ value = getattr(ctx, attr)
505
+ if value is not None:
506
+ span.set_attribute(f"mcp.context.{attr}", str(value))
507
+
508
+ # Also check baggage for session ID
509
+ session_id_from_baggage = baggage.get_baggage("mcp.session.id")
510
+ if session_id_from_baggage:
511
+ span.set_attribute("mcp.session.id", session_id_from_baggage)
512
+
513
+ # Add event for resource read start
514
+ span.add_event("resource.read.started", {
515
+ "resource.uri": resource_uri
516
+ })
395
517
 
396
518
  try:
397
519
  result = func(*args, **kwargs)
398
520
  span.set_status(Status(StatusCode.OK))
399
521
 
400
- # Add result size if applicable
522
+ # Add event for successful read
523
+ span.add_event("resource.read.completed", {
524
+ "resource.uri": resource_uri
525
+ })
526
+
527
+ # Add result metadata
401
528
  if hasattr(result, '__len__'):
402
- span.set_attribute("mcp.resource.size", len(result))
529
+ span.set_attribute("mcp.resource.result.size", len(result))
530
+
531
+ # Determine content type if possible
532
+ if isinstance(result, str):
533
+ span.set_attribute("mcp.resource.result.type", "text")
534
+ span.set_attribute("mcp.resource.result.length", len(result))
535
+ elif isinstance(result, bytes):
536
+ span.set_attribute("mcp.resource.result.type", "binary")
537
+ span.set_attribute("mcp.resource.result.size_bytes", len(result))
538
+ elif isinstance(result, dict):
539
+ span.set_attribute("mcp.resource.result.type", "object")
540
+ span.set_attribute("mcp.resource.result.keys_count", len(result))
541
+ elif isinstance(result, list):
542
+ span.set_attribute("mcp.resource.result.type", "array")
543
+ span.set_attribute("mcp.resource.result.items_count", len(result))
403
544
 
404
545
  return result
405
546
  except Exception as e:
406
547
  span.record_exception(e)
407
548
  span.set_status(Status(StatusCode.ERROR, str(e)))
549
+
550
+ # 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
+ })
408
556
  raise
409
557
 
410
558
  if asyncio.iscoroutinefunction(func):
@@ -424,50 +572,176 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
424
572
 
425
573
  @functools.wraps(func)
426
574
  async def async_wrapper(*args, **kwargs):
427
- with tracer.start_as_current_span(f"prompt.{prompt_name}") as span:
428
- _add_component_attributes(span, "prompt", prompt_name)
575
+ # Create a more descriptive span name
576
+ span_name = f"mcp.prompt.{prompt_name}.generate"
577
+ with tracer.start_as_current_span(span_name) as span:
578
+ # Add comprehensive attributes
579
+ span.set_attribute("mcp.component.type", "prompt")
580
+ span.set_attribute("mcp.component.name", prompt_name)
581
+ span.set_attribute("mcp.prompt.name", prompt_name)
582
+ span.set_attribute("mcp.prompt.function", func.__name__)
583
+ span.set_attribute("mcp.prompt.module", func.__module__ if hasattr(func, '__module__') else "unknown")
584
+ span.set_attribute("mcp.execution.async", True)
429
585
 
430
586
  # Extract Context parameter if present
431
587
  ctx = kwargs.get('ctx')
432
- if ctx and hasattr(ctx, 'request_id'):
433
- span.set_attribute("mcp.request.id", ctx.request_id)
588
+ if ctx:
589
+ # Only extract known MCP context attributes
590
+ ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
591
+ for attr in ctx_attrs:
592
+ if hasattr(ctx, attr):
593
+ value = getattr(ctx, attr)
594
+ if value is not None:
595
+ span.set_attribute(f"mcp.context.{attr}", str(value))
596
+
597
+ # Also check baggage for session ID
598
+ session_id_from_baggage = baggage.get_baggage("mcp.session.id")
599
+ if session_id_from_baggage:
600
+ span.set_attribute("mcp.session.id", session_id_from_baggage)
601
+
602
+ # Add prompt arguments
603
+ for key, value in kwargs.items():
604
+ if key != 'ctx':
605
+ if isinstance(value, (str, int, float, bool)) or value is None:
606
+ span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
607
+ else:
608
+ span.set_attribute(f"mcp.prompt.arg.{key}.type", type(value).__name__)
609
+
610
+ # Add event for prompt generation start
611
+ span.add_event("prompt.generation.started", {
612
+ "prompt.name": prompt_name
613
+ })
434
614
 
435
615
  try:
436
616
  result = await func(*args, **kwargs)
437
617
  span.set_status(Status(StatusCode.OK))
438
618
 
439
- # Add message count if result is a list
619
+ # Add event for successful generation
620
+ span.add_event("prompt.generation.completed", {
621
+ "prompt.name": prompt_name
622
+ })
623
+
624
+ # Add message count and type information
440
625
  if isinstance(result, list):
441
- span.set_attribute("mcp.prompt.message_count", len(result))
626
+ span.set_attribute("mcp.prompt.result.message_count", len(result))
627
+ span.set_attribute("mcp.prompt.result.type", "message_list")
628
+
629
+ # Analyze message types if they have role attributes
630
+ roles = []
631
+ for msg in result:
632
+ if hasattr(msg, 'role'):
633
+ roles.append(msg.role)
634
+ elif isinstance(msg, dict) and 'role' in msg:
635
+ roles.append(msg['role'])
636
+
637
+ if roles:
638
+ 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}))
641
+ elif isinstance(result, str):
642
+ span.set_attribute("mcp.prompt.result.type", "string")
643
+ span.set_attribute("mcp.prompt.result.length", len(result))
644
+ else:
645
+ span.set_attribute("mcp.prompt.result.type", type(result).__name__)
442
646
 
443
647
  return result
444
648
  except Exception as e:
445
649
  span.record_exception(e)
446
650
  span.set_status(Status(StatusCode.ERROR, str(e)))
651
+
652
+ # 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
+ })
447
658
  raise
448
659
 
449
660
  @functools.wraps(func)
450
661
  def sync_wrapper(*args, **kwargs):
451
- with tracer.start_as_current_span(f"prompt.{prompt_name}") as span:
452
- _add_component_attributes(span, "prompt", prompt_name)
662
+ # Create a more descriptive span name
663
+ span_name = f"mcp.prompt.{prompt_name}.generate"
664
+ with tracer.start_as_current_span(span_name) as span:
665
+ # Add comprehensive attributes
666
+ span.set_attribute("mcp.component.type", "prompt")
667
+ span.set_attribute("mcp.component.name", prompt_name)
668
+ span.set_attribute("mcp.prompt.name", prompt_name)
669
+ span.set_attribute("mcp.prompt.function", func.__name__)
670
+ span.set_attribute("mcp.prompt.module", func.__module__ if hasattr(func, '__module__') else "unknown")
671
+ span.set_attribute("mcp.execution.async", False)
453
672
 
454
673
  # Extract Context parameter if present
455
674
  ctx = kwargs.get('ctx')
456
- if ctx and hasattr(ctx, 'request_id'):
457
- span.set_attribute("mcp.request.id", ctx.request_id)
675
+ if ctx:
676
+ # Only extract known MCP context attributes
677
+ ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
678
+ for attr in ctx_attrs:
679
+ if hasattr(ctx, attr):
680
+ value = getattr(ctx, attr)
681
+ if value is not None:
682
+ span.set_attribute(f"mcp.context.{attr}", str(value))
683
+
684
+ # Also check baggage for session ID
685
+ session_id_from_baggage = baggage.get_baggage("mcp.session.id")
686
+ if session_id_from_baggage:
687
+ span.set_attribute("mcp.session.id", session_id_from_baggage)
688
+
689
+ # Add prompt arguments
690
+ for key, value in kwargs.items():
691
+ if key != 'ctx':
692
+ if isinstance(value, (str, int, float, bool)) or value is None:
693
+ span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
694
+ else:
695
+ span.set_attribute(f"mcp.prompt.arg.{key}.type", type(value).__name__)
696
+
697
+ # Add event for prompt generation start
698
+ span.add_event("prompt.generation.started", {
699
+ "prompt.name": prompt_name
700
+ })
458
701
 
459
702
  try:
460
703
  result = func(*args, **kwargs)
461
704
  span.set_status(Status(StatusCode.OK))
462
705
 
463
- # Add message count if result is a list
706
+ # Add event for successful generation
707
+ span.add_event("prompt.generation.completed", {
708
+ "prompt.name": prompt_name
709
+ })
710
+
711
+ # Add message count and type information
464
712
  if isinstance(result, list):
465
- span.set_attribute("mcp.prompt.message_count", len(result))
713
+ span.set_attribute("mcp.prompt.result.message_count", len(result))
714
+ span.set_attribute("mcp.prompt.result.type", "message_list")
715
+
716
+ # Analyze message types if they have role attributes
717
+ roles = []
718
+ for msg in result:
719
+ if hasattr(msg, 'role'):
720
+ roles.append(msg.role)
721
+ elif isinstance(msg, dict) and 'role' in msg:
722
+ roles.append(msg['role'])
723
+
724
+ if roles:
725
+ 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}))
728
+ elif isinstance(result, str):
729
+ span.set_attribute("mcp.prompt.result.type", "string")
730
+ span.set_attribute("mcp.prompt.result.length", len(result))
731
+ else:
732
+ span.set_attribute("mcp.prompt.result.type", type(result).__name__)
466
733
 
467
734
  return result
468
735
  except Exception as e:
469
736
  span.record_exception(e)
470
737
  span.set_status(Status(StatusCode.ERROR, str(e)))
738
+
739
+ # 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
+ })
471
745
  raise
472
746
 
473
747
  if asyncio.iscoroutinefunction(func):
@@ -478,7 +752,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
478
752
  @asynccontextmanager
479
753
  async def telemetry_lifespan(mcp_instance):
480
754
  """Simplified lifespan for telemetry initialization and cleanup."""
481
- global _provider, _instrumented_tools
755
+ global _provider
482
756
 
483
757
  # Initialize telemetry with the server name
484
758
  provider = init_telemetry(service_name=mcp_instance.name)
@@ -496,38 +770,124 @@ async def telemetry_lifespan(mcp_instance):
496
770
 
497
771
  class SessionTracingMiddleware(BaseHTTPMiddleware):
498
772
  async def dispatch(self, request: Request, call_next):
499
- # Extract session ID from query params
773
+ # Extract session ID from query params or headers
500
774
  session_id = request.query_params.get('session_id')
501
- if session_id:
502
- # Add to baggage for propagation
503
- ctx = baggage.set_baggage("mcp.session.id", session_id)
504
- from opentelemetry import context
505
- token = context.attach(ctx)
775
+ if not session_id:
776
+ # Check headers as fallback
777
+ session_id = request.headers.get('x-session-id')
778
+
779
+ # Create a descriptive span name based on the request
780
+ method = request.method
781
+ path = request.url.path
782
+
783
+ # Determine the operation type from the path
784
+ operation_type = "unknown"
785
+ if "/mcp" in path:
786
+ operation_type = "mcp.request"
787
+ elif "/sse" in path:
788
+ operation_type = "sse.stream"
789
+ elif "/auth" in path:
790
+ operation_type = "auth"
791
+
792
+ span_name = f"{operation_type}.{method.lower()}"
793
+
794
+ tracer = get_tracer()
795
+ with tracer.start_as_current_span(span_name) as span:
796
+ # Add comprehensive HTTP attributes
797
+ span.set_attribute("http.method", method)
798
+ span.set_attribute("http.url", str(request.url))
799
+ span.set_attribute("http.scheme", request.url.scheme)
800
+ span.set_attribute("http.host", request.url.hostname or "unknown")
801
+ span.set_attribute("http.target", path)
802
+ span.set_attribute("http.user_agent", request.headers.get("user-agent", "unknown"))
506
803
 
507
- # Also create a span for the HTTP request
508
- tracer = get_tracer()
509
- with tracer.start_as_current_span(f"http.{request.method} {request.url.path}") as span:
510
- span.set_attribute("http.method", request.method)
511
- span.set_attribute("http.url", str(request.url))
512
- span.set_attribute("http.session_id", session_id)
804
+ # Add session tracking
805
+ if session_id:
513
806
  span.set_attribute("mcp.session.id", session_id)
807
+ # Add to baggage for propagation
808
+ ctx = baggage.set_baggage("mcp.session.id", session_id)
809
+ from opentelemetry import context
810
+ token = context.attach(ctx)
811
+ else:
812
+ token = None
813
+
814
+ # Add request size if available
815
+ content_length = request.headers.get("content-length")
816
+ if content_length:
817
+ span.set_attribute("http.request.size", int(content_length))
818
+
819
+ # Add event for request start
820
+ span.add_event("http.request.started", {
821
+ "method": method,
822
+ "path": path
823
+ })
824
+
825
+ try:
826
+ response = await call_next(request)
827
+
828
+ # Add response attributes
829
+ span.set_attribute("http.status_code", response.status_code)
830
+ span.set_attribute("http.status_class", f"{response.status_code // 100}xx")
514
831
 
515
- try:
516
- response = await call_next(request)
517
- span.set_attribute("http.status_code", response.status_code)
518
- return response
519
- finally:
832
+ # Set span status based on HTTP status
833
+ if response.status_code >= 400:
834
+ span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
835
+ else:
836
+ span.set_status(Status(StatusCode.OK))
837
+
838
+ # 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
+
845
+ return response
846
+ except Exception as e:
847
+ span.record_exception(e)
848
+ span.set_status(Status(StatusCode.ERROR, str(e)))
849
+
850
+ # 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
+ })
857
+ raise
858
+ finally:
859
+ if token:
520
860
  context.detach(token)
521
- else:
522
- return await call_next(request)
523
861
 
524
862
  # Try to add middleware to FastMCP app if it has Starlette app
525
863
  if hasattr(mcp_instance, 'app') or hasattr(mcp_instance, '_app'):
526
864
  app = getattr(mcp_instance, 'app', getattr(mcp_instance, '_app', None))
527
865
  if app and hasattr(app, 'add_middleware'):
528
866
  app.add_middleware(SessionTracingMiddleware)
529
- except Exception:
530
- pass
867
+ print("[TELEMETRY DEBUG] Added SessionTracingMiddleware to FastMCP app")
868
+
869
+ # 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")
872
+ # The tools should already be instrumented when they were registered
873
+
874
+ # Try to patch FastMCP's request handling to ensure context propagation
875
+ if hasattr(mcp_instance, 'handle_request'):
876
+ original_handle_request = mcp_instance.handle_request
877
+
878
+ async def traced_handle_request(*args, **kwargs):
879
+ tracer = get_tracer()
880
+ with tracer.start_as_current_span("mcp.handle_request") as span:
881
+ span.set_attribute("mcp.request.handler", "handle_request")
882
+ return await original_handle_request(*args, **kwargs)
883
+
884
+ mcp_instance.handle_request = traced_handle_request
885
+ print("[TELEMETRY DEBUG] Patched FastMCP handle_request method")
886
+
887
+ except Exception as e:
888
+ print(f"[TELEMETRY DEBUG] Error setting up telemetry middleware: {e}")
889
+ import traceback
890
+ traceback.print_exc()
531
891
 
532
892
  try:
533
893
  # Yield control back to FastMCP