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