golf-mcp 0.1.19__py3-none-any.whl → 0.2.0__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 (123) hide show
  1. golf/__init__.py +9 -1
  2. golf/_endpoints.py +6 -0
  3. golf/_endpoints_fallback.py +10 -0
  4. golf/auth/__init__.py +188 -84
  5. golf/auth/api_key.py +6 -14
  6. golf/auth/factory.py +333 -0
  7. golf/auth/helpers.py +12 -42
  8. golf/auth/providers.py +396 -0
  9. golf/auth/registry.py +256 -0
  10. golf/cli/branding.py +192 -0
  11. golf/cli/main.py +28 -69
  12. golf/commands/__init__.py +2 -0
  13. golf/commands/build.py +4 -7
  14. golf/commands/init.py +30 -53
  15. golf/commands/run.py +50 -20
  16. golf/core/builder.py +356 -412
  17. golf/core/builder_auth.py +63 -144
  18. golf/core/builder_telemetry.py +26 -3
  19. golf/core/config.py +38 -59
  20. golf/core/parser.py +132 -139
  21. golf/core/platform.py +12 -10
  22. golf/core/telemetry.py +11 -19
  23. golf/core/transformer.py +38 -15
  24. golf/examples/__pycache__/__init__.cpython-311.pyc +0 -0
  25. golf/examples/basic/.coverage +0 -0
  26. golf/examples/basic/.env.example +8 -4
  27. golf/examples/basic/README.md +117 -45
  28. golf/examples/basic/__pycache__/auth.cpython-311.pyc +0 -0
  29. golf/examples/basic/auth.py +76 -0
  30. golf/examples/basic/golf.json +2 -5
  31. golf/examples/basic/htmlcov/.gitignore +2 -0
  32. golf/examples/basic/htmlcov/class_index.html +547 -0
  33. golf/examples/basic/htmlcov/coverage_html_cb_6fb7b396.js +733 -0
  34. golf/examples/basic/htmlcov/favicon_32_cb_58284776.png +0 -0
  35. golf/examples/basic/htmlcov/function_index.html +2091 -0
  36. golf/examples/basic/htmlcov/index.html +349 -0
  37. golf/examples/basic/htmlcov/keybd_closed_cb_ce680311.png +0 -0
  38. golf/examples/basic/htmlcov/status.json +1 -0
  39. golf/examples/basic/htmlcov/style_cb_8e611ae1.css +337 -0
  40. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496___init___py.html +323 -0
  41. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_api_key_py.html +170 -0
  42. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_factory_py.html +430 -0
  43. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_helpers_py.html +288 -0
  44. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_providers_py.html +493 -0
  45. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_registry_py.html +353 -0
  46. golf/examples/basic/htmlcov/z_3ec3b3f490dc0950___init___py.html +120 -0
  47. golf/examples/basic/htmlcov/z_3ec3b3f490dc0950_instrumentation_py.html +1535 -0
  48. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db___init___py.html +98 -0
  49. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_branding_py.html +289 -0
  50. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_main_py.html +476 -0
  51. golf/examples/basic/htmlcov/z_5a6c4e6bcc86fb2f___init___py.html +97 -0
  52. golf/examples/basic/htmlcov/z_6cadab9ec0df475d___init___py.html +102 -0
  53. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_build_py.html +178 -0
  54. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_init_py.html +387 -0
  55. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_run_py.html +222 -0
  56. golf/examples/basic/htmlcov/z_6fcdee0582ba84e4___init___py.html +106 -0
  57. golf/examples/basic/htmlcov/z_6fcdee0582ba84e4__endpoints_fallback_py.html +107 -0
  58. golf/examples/basic/htmlcov/z_7ba499ed22986217___init___py.html +98 -0
  59. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_auth_py.html +306 -0
  60. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_metrics_py.html +329 -0
  61. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_py.html +1471 -0
  62. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_telemetry_py.html +186 -0
  63. golf/examples/basic/htmlcov/z_7ba499ed22986217_config_py.html +315 -0
  64. golf/examples/basic/htmlcov/z_7ba499ed22986217_parser_py.html +1149 -0
  65. golf/examples/basic/htmlcov/z_7ba499ed22986217_platform_py.html +279 -0
  66. golf/examples/basic/htmlcov/z_7ba499ed22986217_telemetry_py.html +589 -0
  67. golf/examples/basic/htmlcov/z_7ba499ed22986217_transformer_py.html +286 -0
  68. golf/examples/basic/htmlcov/z_7d7da37693a43688___init___py.html +107 -0
  69. golf/examples/basic/htmlcov/z_7d7da37693a43688_collector_py.html +417 -0
  70. golf/examples/basic/htmlcov/z_7d7da37693a43688_registry_py.html +109 -0
  71. golf/examples/basic/htmlcov/z_abe733142b40ad4e___init___py.html +109 -0
  72. golf/examples/basic/htmlcov/z_abe733142b40ad4e_context_py.html +150 -0
  73. golf/examples/basic/htmlcov/z_abe733142b40ad4e_elicitation_py.html +267 -0
  74. golf/examples/basic/htmlcov/z_abe733142b40ad4e_sampling_py.html +318 -0
  75. golf/examples/basic/prompts/__pycache__/welcome.cpython-311.pyc +0 -0
  76. golf/examples/basic/prompts/welcome.py +3 -5
  77. golf/examples/basic/resources/__pycache__/current_time.cpython-311.pyc +0 -0
  78. golf/examples/basic/resources/__pycache__/info.cpython-311.pyc +0 -0
  79. golf/examples/basic/resources/current_time.py +5 -13
  80. golf/examples/basic/resources/weather/__pycache__/common.cpython-311.pyc +0 -0
  81. golf/examples/basic/resources/weather/__pycache__/current.cpython-311.pyc +0 -0
  82. golf/examples/basic/resources/weather/__pycache__/forecast.cpython-311.pyc +0 -0
  83. golf/examples/basic/resources/weather/city.py +46 -0
  84. golf/examples/basic/resources/weather/common.py +4 -11
  85. golf/examples/basic/resources/weather/current.py +5 -5
  86. golf/examples/basic/resources/weather/forecast.py +5 -5
  87. golf/examples/basic/tools/__pycache__/calculator.cpython-311.pyc +0 -0
  88. golf/examples/basic/tools/calculator.py +94 -0
  89. golf/examples/basic/tools/say/__pycache__/hello.cpython-311.pyc +0 -0
  90. golf/examples/basic/tools/say/hello.py +65 -0
  91. golf/metrics/collector.py +100 -19
  92. golf/telemetry/__init__.py +4 -0
  93. golf/telemetry/instrumentation.py +496 -174
  94. golf/utilities/__init__.py +12 -0
  95. golf/utilities/context.py +53 -0
  96. golf/utilities/elicitation.py +170 -0
  97. golf/utilities/sampling.py +221 -0
  98. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/METADATA +56 -110
  99. golf_mcp-0.2.0.dist-info/RECORD +110 -0
  100. golf/auth/oauth.py +0 -861
  101. golf/auth/provider.py +0 -115
  102. golf/examples/api_key/.env +0 -2
  103. golf/examples/api_key/.env.example +0 -1
  104. golf/examples/api_key/README.md +0 -84
  105. golf/examples/api_key/golf.json +0 -8
  106. golf/examples/api_key/pre_build.py +0 -11
  107. golf/examples/api_key/tools/issues/create.py +0 -93
  108. golf/examples/api_key/tools/issues/list.py +0 -92
  109. golf/examples/api_key/tools/repos/list.py +0 -111
  110. golf/examples/api_key/tools/search/code.py +0 -106
  111. golf/examples/api_key/tools/users/get.py +0 -82
  112. golf/examples/basic/.env +0 -5
  113. golf/examples/basic/pre_build.py +0 -28
  114. golf/examples/basic/tools/github_user.py +0 -65
  115. golf/examples/basic/tools/hello.py +0 -34
  116. golf/examples/basic/tools/payments/charge.py +0 -70
  117. golf/examples/basic/tools/payments/common.py +0 -36
  118. golf/examples/basic/tools/payments/refund.py +0 -61
  119. golf_mcp-0.1.19.dist-info/RECORD +0 -60
  120. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/WHEEL +0 -0
  121. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  122. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  123. {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/top_level.txt +0 -0
@@ -5,12 +5,22 @@ import functools
5
5
  import os
6
6
  import sys
7
7
  import time
8
+ import json
8
9
  from collections.abc import Callable
9
10
  from contextlib import asynccontextmanager
10
- from typing import TypeVar
11
+ from typing import Any, TypeVar
12
+ from collections.abc import AsyncGenerator
11
13
  from collections import OrderedDict
12
14
 
13
15
  from opentelemetry import baggage, trace
16
+
17
+ # Import endpoints with fallback for dev mode
18
+ try:
19
+ # In built wheels, this exists (generated from _endpoints.py.in)
20
+ from golf import _endpoints # type: ignore
21
+ except ImportError:
22
+ # In editable/dev installs, fall back to env-based values
23
+ from golf import _endpoints_fallback as _endpoints # type: ignore
14
24
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
15
25
  from opentelemetry.sdk.resources import Resource
16
26
  from opentelemetry.sdk.trace import TracerProvider
@@ -18,13 +28,38 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExport
18
28
  from opentelemetry.trace import Status, StatusCode
19
29
 
20
30
  from starlette.middleware.base import BaseHTTPMiddleware
21
- from starlette.requests import Request
22
31
 
23
32
  T = TypeVar("T")
24
33
 
25
34
  # Global tracer instance
26
35
  _tracer: trace.Tracer | None = None
27
36
  _provider: TracerProvider | None = None
37
+ _detailed_tracing_enabled: bool = False
38
+
39
+
40
+ def _safe_serialize(data: Any, max_length: int = 1000) -> str | None:
41
+ """Safely serialize data to string with length limit."""
42
+ try:
43
+ if isinstance(data, str):
44
+ serialized = data
45
+ else:
46
+ serialized = json.dumps(data, default=str, ensure_ascii=False)
47
+
48
+ if len(serialized) > max_length:
49
+ return serialized[:max_length] + "..." + f" (truncated from {len(serialized)} chars)"
50
+ return serialized
51
+ except (TypeError, ValueError):
52
+ # Fallback for non-serializable objects
53
+ try:
54
+ return str(data)[:max_length] + "..." if len(str(data)) > max_length else str(data)
55
+ except Exception:
56
+ return None
57
+
58
+
59
+ def set_detailed_tracing(enabled: bool) -> None:
60
+ """Enable or disable detailed tracing with input/output capture."""
61
+ global _detailed_tracing_enabled
62
+ _detailed_tracing_enabled = enabled
28
63
 
29
64
 
30
65
  def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | None:
@@ -36,12 +71,25 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
36
71
 
37
72
  # Check for Golf platform integration first
38
73
  golf_api_key = os.environ.get("GOLF_API_KEY")
39
- if golf_api_key and not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
40
- # Auto-configure for Golf platform
74
+ if golf_api_key:
75
+ # Auto-configure for Golf platform - always use OTLP when Golf API
76
+ # key is present
41
77
  os.environ["OTEL_TRACES_EXPORTER"] = "otlp_http"
42
- os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:8000/api/v1/otel"
43
- os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"X-Golf-Key={golf_api_key}"
44
- print("[INFO] Auto-configured OpenTelemetry for Golf platform ingestion")
78
+
79
+ # Only set endpoint if not already configured (allow user override)
80
+ if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
81
+ os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = _endpoints.OTEL_ENDPOINT
82
+
83
+ # Set Golf platform headers (append to existing if present)
84
+ existing_headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
85
+ golf_header = f"X-Golf-Key={golf_api_key}"
86
+
87
+ if existing_headers:
88
+ # Check if Golf key is already in headers
89
+ if "X-Golf-Key=" not in existing_headers:
90
+ os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"{existing_headers},{golf_header}"
91
+ else:
92
+ os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = golf_header
45
93
 
46
94
  # Check for required environment variables based on exporter type
47
95
  exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
@@ -78,9 +126,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
78
126
  # Configure exporter based on type
79
127
  try:
80
128
  if exporter_type == "otlp_http":
81
- endpoint = os.environ.get(
82
- "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
83
- )
129
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
84
130
  headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
85
131
 
86
132
  # Parse headers if provided
@@ -91,13 +137,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
91
137
  key, value = header.split("=", 1)
92
138
  header_dict[key.strip()] = value.strip()
93
139
 
94
- exporter = OTLPSpanExporter(
95
- endpoint=endpoint, headers=header_dict if header_dict else None
96
- )
140
+ exporter = OTLPSpanExporter(endpoint=endpoint, headers=header_dict if header_dict else None)
97
141
 
98
- # Log successful configuration for Golf platform
99
- if golf_api_key:
100
- print(f"[INFO] OpenTelemetry configured for Golf platform: {endpoint}")
101
142
  else:
102
143
  # Default to console exporter
103
144
  exporter = ConsoleSpanExporter(out=sys.stderr)
@@ -112,7 +153,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
112
153
  processor = BatchSpanProcessor(
113
154
  exporter,
114
155
  max_queue_size=2048,
115
- schedule_delay_millis=1000, # Export every 1 second instead of default 5 seconds
156
+ schedule_delay_millis=1000, # Export every 1 second instead of
157
+ # default 5 seconds
116
158
  max_export_batch_size=512,
117
159
  export_timeout_millis=5000,
118
160
  )
@@ -127,10 +169,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
127
169
  try:
128
170
  # Check if a provider is already set to avoid the warning
129
171
  existing_provider = trace.get_tracer_provider()
130
- if (
131
- existing_provider is None
132
- or str(type(existing_provider).__name__) == "ProxyTracerProvider"
133
- ):
172
+ if existing_provider is None or str(type(existing_provider).__name__) == "ProxyTracerProvider":
134
173
  # Only set if no provider exists or it's the default proxy provider
135
174
  trace.set_tracer_provider(provider)
136
175
  _provider = provider
@@ -167,7 +206,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
167
206
  tracer = get_tracer()
168
207
 
169
208
  @functools.wraps(func)
170
- async def async_wrapper(*args, **kwargs):
209
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
171
210
  # Record metrics timing
172
211
  import time
173
212
 
@@ -178,20 +217,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
178
217
 
179
218
  # start_as_current_span automatically uses the current context and manages it
180
219
  with tracer.start_as_current_span(span_name) as span:
181
- # Add comprehensive attributes
220
+ # Add essential attributes only
182
221
  span.set_attribute("mcp.component.type", "tool")
183
- span.set_attribute("mcp.component.name", tool_name)
184
222
  span.set_attribute("mcp.tool.name", tool_name)
185
- span.set_attribute("mcp.tool.function", func.__name__)
186
223
  span.set_attribute(
187
224
  "mcp.tool.module",
188
225
  func.__module__ if hasattr(func, "__module__") else "unknown",
189
226
  )
190
227
 
191
- # Add execution context
192
- span.set_attribute("mcp.execution.args_count", len(args))
193
- span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
194
- span.set_attribute("mcp.execution.async", True)
228
+ # Add minimal execution context
229
+ if args or kwargs:
230
+ span.set_attribute("mcp.execution.has_params", True)
231
+
232
+ # Capture inputs if detailed tracing is enabled
233
+ if _detailed_tracing_enabled and (args or kwargs):
234
+ input_data = {"args": args, "kwargs": kwargs} if args or kwargs else None
235
+ if input_data:
236
+ input_str = _safe_serialize(input_data)
237
+ if input_str:
238
+ span.set_attribute("mcp.tool.input", input_str)
195
239
 
196
240
  # Extract Context parameter if present
197
241
  ctx = kwargs.get("ctx")
@@ -231,42 +275,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
231
275
 
232
276
  metrics_collector = get_metrics_collector()
233
277
  metrics_collector.increment_tool_execution(tool_name, "success")
234
- metrics_collector.record_tool_duration(
235
- tool_name, time.time() - start_time
236
- )
278
+ metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
237
279
  except ImportError:
238
280
  # Metrics not available, continue without metrics
239
281
  pass
240
282
 
241
- # Capture result metadata with better structure
283
+ # Capture result metadata
242
284
  if result is not None:
243
- if isinstance(result, str | int | float | bool):
244
- span.set_attribute("mcp.tool.result.value", str(result))
245
- span.set_attribute(
246
- "mcp.tool.result.type", type(result).__name__
247
- )
248
- elif isinstance(result, list):
249
- span.set_attribute("mcp.tool.result.count", len(result))
250
- span.set_attribute("mcp.tool.result.type", "array")
251
- elif isinstance(result, dict):
252
- span.set_attribute("mcp.tool.result.count", len(result))
253
- span.set_attribute("mcp.tool.result.type", "object")
254
- # Only show first few keys to avoid exceeding attribute limits
255
- if len(result) > 0 and len(result) <= 5:
256
- keys_list = list(result.keys())[:5]
257
- # Limit key length and join
258
- truncated_keys = [
259
- str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
260
- for k in keys_list
261
- ]
262
- span.set_attribute(
263
- "mcp.tool.result.sample_keys", ",".join(truncated_keys)
264
- )
265
- elif hasattr(result, "__len__"):
285
+ span.set_attribute("mcp.tool.result.type", type(result).__name__)
286
+
287
+ if isinstance(result, list | dict) and hasattr(result, "__len__"):
288
+ span.set_attribute("mcp.tool.result.size", len(result))
289
+ elif isinstance(result, str):
266
290
  span.set_attribute("mcp.tool.result.length", len(result))
267
291
 
268
- # For any result, record its type
269
- span.set_attribute("mcp.tool.result.class", type(result).__name__)
292
+ # Capture full output if detailed tracing is enabled
293
+ if _detailed_tracing_enabled:
294
+ output_str = _safe_serialize(result)
295
+ if output_str:
296
+ span.set_attribute("mcp.tool.output", output_str)
270
297
 
271
298
  return result
272
299
  except Exception as e:
@@ -297,7 +324,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
297
324
  raise
298
325
 
299
326
  @functools.wraps(func)
300
- def sync_wrapper(*args, **kwargs):
327
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
301
328
  # Record metrics timing
302
329
  import time
303
330
 
@@ -308,11 +335,9 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
308
335
 
309
336
  # start_as_current_span automatically uses the current context and manages it
310
337
  with tracer.start_as_current_span(span_name) as span:
311
- # Add comprehensive attributes
338
+ # Add essential attributes only
312
339
  span.set_attribute("mcp.component.type", "tool")
313
- span.set_attribute("mcp.component.name", tool_name)
314
340
  span.set_attribute("mcp.tool.name", tool_name)
315
- span.set_attribute("mcp.tool.function", func.__name__)
316
341
  span.set_attribute(
317
342
  "mcp.tool.module",
318
343
  func.__module__ if hasattr(func, "__module__") else "unknown",
@@ -321,7 +346,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
321
346
  # Add execution context
322
347
  span.set_attribute("mcp.execution.args_count", len(args))
323
348
  span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
324
- span.set_attribute("mcp.execution.async", False)
325
349
 
326
350
  # Extract Context parameter if present
327
351
  ctx = kwargs.get("ctx")
@@ -361,42 +385,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
361
385
 
362
386
  metrics_collector = get_metrics_collector()
363
387
  metrics_collector.increment_tool_execution(tool_name, "success")
364
- metrics_collector.record_tool_duration(
365
- tool_name, time.time() - start_time
366
- )
388
+ metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
367
389
  except ImportError:
368
390
  # Metrics not available, continue without metrics
369
391
  pass
370
392
 
371
- # Capture result metadata with better structure
393
+ # Capture result metadata
372
394
  if result is not None:
373
- if isinstance(result, str | int | float | bool):
374
- span.set_attribute("mcp.tool.result.value", str(result))
375
- span.set_attribute(
376
- "mcp.tool.result.type", type(result).__name__
377
- )
378
- elif isinstance(result, list):
379
- span.set_attribute("mcp.tool.result.count", len(result))
380
- span.set_attribute("mcp.tool.result.type", "array")
381
- elif isinstance(result, dict):
382
- span.set_attribute("mcp.tool.result.count", len(result))
383
- span.set_attribute("mcp.tool.result.type", "object")
384
- # Only show first few keys to avoid exceeding attribute limits
385
- if len(result) > 0 and len(result) <= 5:
386
- keys_list = list(result.keys())[:5]
387
- # Limit key length and join
388
- truncated_keys = [
389
- str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
390
- for k in keys_list
391
- ]
392
- span.set_attribute(
393
- "mcp.tool.result.sample_keys", ",".join(truncated_keys)
394
- )
395
- elif hasattr(result, "__len__"):
395
+ span.set_attribute("mcp.tool.result.type", type(result).__name__)
396
+
397
+ if isinstance(result, list | dict) and hasattr(result, "__len__"):
398
+ span.set_attribute("mcp.tool.result.size", len(result))
399
+ elif isinstance(result, str):
396
400
  span.set_attribute("mcp.tool.result.length", len(result))
397
401
 
398
- # For any result, record its type
399
- span.set_attribute("mcp.tool.result.class", type(result).__name__)
402
+ # Capture full output if detailed tracing is enabled
403
+ if _detailed_tracing_enabled:
404
+ output_str = _safe_serialize(result)
405
+ if output_str:
406
+ span.set_attribute("mcp.tool.output", output_str)
400
407
 
401
408
  return result
402
409
  except Exception as e:
@@ -447,21 +454,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
447
454
  is_template = "{" in resource_uri
448
455
 
449
456
  @functools.wraps(func)
450
- async def async_wrapper(*args, **kwargs):
457
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
451
458
  # Create a more descriptive span name
452
459
  span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
453
460
  with tracer.start_as_current_span(span_name) as span:
454
- # Add comprehensive attributes
461
+ # Add essential attributes only
455
462
  span.set_attribute("mcp.component.type", "resource")
456
- span.set_attribute("mcp.component.name", resource_uri)
457
463
  span.set_attribute("mcp.resource.uri", resource_uri)
458
464
  span.set_attribute("mcp.resource.is_template", is_template)
459
- span.set_attribute("mcp.resource.function", func.__name__)
460
465
  span.set_attribute(
461
466
  "mcp.resource.module",
462
467
  func.__module__ if hasattr(func, "__module__") else "unknown",
463
468
  )
464
- span.set_attribute("mcp.execution.async", True)
465
469
 
466
470
  # Extract Context parameter if present
467
471
  ctx = kwargs.get("ctx")
@@ -493,9 +497,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
493
497
  span.set_status(Status(StatusCode.OK))
494
498
 
495
499
  # Add event for successful read
496
- span.add_event(
497
- "resource.read.completed", {"resource.uri": resource_uri}
498
- )
500
+ span.add_event("resource.read.completed", {"resource.uri": resource_uri})
499
501
 
500
502
  # Add result metadata
501
503
  if hasattr(result, "__len__"):
@@ -532,21 +534,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
532
534
  raise
533
535
 
534
536
  @functools.wraps(func)
535
- def sync_wrapper(*args, **kwargs):
537
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
536
538
  # Create a more descriptive span name
537
539
  span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
538
540
  with tracer.start_as_current_span(span_name) as span:
539
- # Add comprehensive attributes
541
+ # Add essential attributes only
540
542
  span.set_attribute("mcp.component.type", "resource")
541
- span.set_attribute("mcp.component.name", resource_uri)
542
543
  span.set_attribute("mcp.resource.uri", resource_uri)
543
544
  span.set_attribute("mcp.resource.is_template", is_template)
544
- span.set_attribute("mcp.resource.function", func.__name__)
545
545
  span.set_attribute(
546
546
  "mcp.resource.module",
547
547
  func.__module__ if hasattr(func, "__module__") else "unknown",
548
548
  )
549
- span.set_attribute("mcp.execution.async", False)
550
549
 
551
550
  # Extract Context parameter if present
552
551
  ctx = kwargs.get("ctx")
@@ -578,9 +577,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
578
577
  span.set_status(Status(StatusCode.OK))
579
578
 
580
579
  # Add event for successful read
581
- span.add_event(
582
- "resource.read.completed", {"resource.uri": resource_uri}
583
- )
580
+ span.add_event("resource.read.completed", {"resource.uri": resource_uri})
584
581
 
585
582
  # Add result metadata
586
583
  if hasattr(result, "__len__"):
@@ -622,6 +619,370 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
622
619
  return sync_wrapper
623
620
 
624
621
 
622
+ def instrument_elicitation(func: Callable[..., T], elicitation_type: str = "elicit") -> Callable[..., T]:
623
+ """Instrument an elicitation function with OpenTelemetry tracing."""
624
+ global _provider
625
+
626
+ # If telemetry is disabled, return the original function
627
+ if _provider is None:
628
+ return func
629
+
630
+ tracer = get_tracer()
631
+
632
+ @functools.wraps(func)
633
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
634
+ # If telemetry is disabled at runtime, call original function
635
+ global _provider
636
+ if _provider is None:
637
+ return await func(*args, **kwargs)
638
+
639
+ # Record metrics timing
640
+ start_time = time.time()
641
+
642
+ # Create a more descriptive span name
643
+ span_name = f"mcp.elicitation.{elicitation_type}.request"
644
+ with tracer.start_as_current_span(span_name) as span:
645
+ # Add essential attributes
646
+ span.set_attribute("mcp.component.type", "elicitation")
647
+ span.set_attribute("mcp.elicitation.type", elicitation_type)
648
+
649
+ # Capture elicitation parameters if detailed tracing is enabled
650
+ if _detailed_tracing_enabled:
651
+ # Extract message from first argument (common pattern)
652
+ if args:
653
+ message = args[0] if isinstance(args[0], str) else None
654
+ if message:
655
+ span.set_attribute("mcp.elicitation.message", _safe_serialize(message, 500))
656
+
657
+ # Extract response_type from kwargs/args
658
+ response_type = kwargs.get("response_type") or (args[1] if len(args) > 1 else None)
659
+ if response_type is not None:
660
+ if isinstance(response_type, list):
661
+ span.set_attribute("mcp.elicitation.response_type", "choice")
662
+ span.set_attribute("mcp.elicitation.choices", str(response_type))
663
+ elif hasattr(response_type, "__name__"):
664
+ span.set_attribute("mcp.elicitation.response_type", response_type.__name__)
665
+ else:
666
+ span.set_attribute("mcp.elicitation.response_type", str(type(response_type).__name__))
667
+
668
+ # Extract Context parameter if present
669
+ ctx = kwargs.get("ctx")
670
+ if ctx:
671
+ ctx_attrs = ["request_id", "session_id", "client_id", "user_id", "tenant_id"]
672
+ for attr in ctx_attrs:
673
+ if hasattr(ctx, attr):
674
+ value = getattr(ctx, attr)
675
+ if value is not None:
676
+ span.set_attribute(f"mcp.context.{attr}", str(value))
677
+
678
+ # Add event for elicitation start
679
+ span.add_event("elicitation.request.started")
680
+
681
+ try:
682
+ result = await func(*args, **kwargs)
683
+ span.set_status(Status(StatusCode.OK))
684
+
685
+ # Add event for successful completion
686
+ span.add_event("elicitation.request.completed")
687
+
688
+ # Capture result metadata
689
+ if result is not None and _detailed_tracing_enabled:
690
+ if isinstance(result, str):
691
+ span.set_attribute("mcp.elicitation.result.content", _safe_serialize(result, 500))
692
+ elif isinstance(result, (list, dict)) and hasattr(result, "__len__"):
693
+ span.set_attribute("mcp.elicitation.result.size", len(result))
694
+ span.set_attribute("mcp.elicitation.result.content", _safe_serialize(result, 1000))
695
+
696
+ # Record metrics for successful elicitation
697
+ try:
698
+ from golf.metrics import get_metrics_collector
699
+
700
+ metrics_collector = get_metrics_collector()
701
+ metrics_collector.increment_elicitation(elicitation_type, "success")
702
+ metrics_collector.record_elicitation_duration(elicitation_type, time.time() - start_time)
703
+ except ImportError:
704
+ pass
705
+
706
+ return result
707
+ except Exception as e:
708
+ span.record_exception(e)
709
+ span.set_status(Status(StatusCode.ERROR, str(e)))
710
+
711
+ # Add event for error
712
+ span.add_event(
713
+ "elicitation.request.error",
714
+ {
715
+ "error.type": type(e).__name__,
716
+ "error.message": str(e),
717
+ },
718
+ )
719
+
720
+ # Record metrics for failed elicitation
721
+ try:
722
+ from golf.metrics import get_metrics_collector
723
+
724
+ metrics_collector = get_metrics_collector()
725
+ metrics_collector.increment_elicitation(elicitation_type, "error")
726
+ metrics_collector.increment_error("elicitation", type(e).__name__)
727
+ except ImportError:
728
+ pass
729
+
730
+ raise
731
+
732
+ @functools.wraps(func)
733
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
734
+ # If telemetry is disabled at runtime, call original function
735
+ global _provider
736
+ if _provider is None:
737
+ return func(*args, **kwargs)
738
+
739
+ # Record metrics timing
740
+ start_time = time.time()
741
+
742
+ # Create a more descriptive span name
743
+ span_name = f"mcp.elicitation.{elicitation_type}.request"
744
+ with tracer.start_as_current_span(span_name) as span:
745
+ # Add essential attributes
746
+ span.set_attribute("mcp.component.type", "elicitation")
747
+ span.set_attribute("mcp.elicitation.type", elicitation_type)
748
+
749
+ # Capture elicitation parameters if detailed tracing is enabled
750
+ if _detailed_tracing_enabled:
751
+ if args:
752
+ message = args[0] if isinstance(args[0], str) else None
753
+ if message:
754
+ span.set_attribute("mcp.elicitation.message", _safe_serialize(message, 500))
755
+
756
+ # Add event for elicitation start
757
+ span.add_event("elicitation.request.started")
758
+
759
+ try:
760
+ result = func(*args, **kwargs)
761
+ span.set_status(Status(StatusCode.OK))
762
+
763
+ # Add event for successful completion
764
+ span.add_event("elicitation.request.completed")
765
+
766
+ # Record metrics for successful elicitation
767
+ try:
768
+ from golf.metrics import get_metrics_collector
769
+
770
+ metrics_collector = get_metrics_collector()
771
+ metrics_collector.increment_elicitation(elicitation_type, "success")
772
+ metrics_collector.record_elicitation_duration(elicitation_type, time.time() - start_time)
773
+ except ImportError:
774
+ pass
775
+
776
+ return result
777
+ except Exception as e:
778
+ span.record_exception(e)
779
+ span.set_status(Status(StatusCode.ERROR, str(e)))
780
+
781
+ # Add event for error
782
+ span.add_event(
783
+ "elicitation.request.error",
784
+ {
785
+ "error.type": type(e).__name__,
786
+ "error.message": str(e),
787
+ },
788
+ )
789
+
790
+ # Record metrics for failed elicitation
791
+ try:
792
+ from golf.metrics import get_metrics_collector
793
+
794
+ metrics_collector = get_metrics_collector()
795
+ metrics_collector.increment_elicitation(elicitation_type, "error")
796
+ metrics_collector.increment_error("elicitation", type(e).__name__)
797
+ except ImportError:
798
+ pass
799
+
800
+ raise
801
+
802
+ if asyncio.iscoroutinefunction(func):
803
+ return async_wrapper
804
+ else:
805
+ return sync_wrapper
806
+
807
+
808
+ def instrument_sampling(func: Callable[..., T], sampling_type: str = "sample") -> Callable[..., T]:
809
+ """Instrument a sampling function with OpenTelemetry tracing."""
810
+ global _provider
811
+
812
+ # If telemetry is disabled, return the original function
813
+ if _provider is None:
814
+ return func
815
+
816
+ tracer = get_tracer()
817
+
818
+ @functools.wraps(func)
819
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
820
+ # If telemetry is disabled at runtime, call original function
821
+ global _provider
822
+ if _provider is None:
823
+ return await func(*args, **kwargs)
824
+
825
+ # Record metrics timing
826
+ start_time = time.time()
827
+
828
+ # Create a more descriptive span name
829
+ span_name = f"mcp.sampling.{sampling_type}.request"
830
+ with tracer.start_as_current_span(span_name) as span:
831
+ # Add essential attributes
832
+ span.set_attribute("mcp.component.type", "sampling")
833
+ span.set_attribute("mcp.sampling.type", sampling_type)
834
+
835
+ # Capture sampling parameters
836
+ messages = kwargs.get("messages") or (args[0] if args else None)
837
+ if messages and _detailed_tracing_enabled:
838
+ if isinstance(messages, str):
839
+ span.set_attribute("mcp.sampling.messages.content", _safe_serialize(messages, 1000))
840
+ elif isinstance(messages, list):
841
+ span.set_attribute("mcp.sampling.messages.type", "list")
842
+ span.set_attribute("mcp.sampling.messages.count", len(messages))
843
+ span.set_attribute("mcp.sampling.messages.content", _safe_serialize(messages, 1000))
844
+
845
+ # Capture other sampling parameters
846
+ system_prompt = kwargs.get("system_prompt")
847
+ if system_prompt and _detailed_tracing_enabled:
848
+ span.set_attribute("mcp.sampling.system_prompt.length", len(str(system_prompt)))
849
+ span.set_attribute("mcp.sampling.system_prompt.content", _safe_serialize(system_prompt, 500))
850
+
851
+ temperature = kwargs.get("temperature")
852
+ if temperature is not None:
853
+ span.set_attribute("mcp.sampling.temperature", temperature)
854
+
855
+ max_tokens = kwargs.get("max_tokens")
856
+ if max_tokens is not None:
857
+ span.set_attribute("mcp.sampling.max_tokens", max_tokens)
858
+
859
+ model_preferences = kwargs.get("model_preferences")
860
+ if model_preferences:
861
+ if isinstance(model_preferences, str):
862
+ span.set_attribute("mcp.sampling.model_preferences", model_preferences)
863
+ elif isinstance(model_preferences, list):
864
+ span.set_attribute("mcp.sampling.model_preferences", ",".join(model_preferences))
865
+
866
+ # Extract Context parameter if present
867
+ ctx = kwargs.get("ctx")
868
+ if ctx:
869
+ ctx_attrs = ["request_id", "session_id", "client_id", "user_id", "tenant_id"]
870
+ for attr in ctx_attrs:
871
+ if hasattr(ctx, attr):
872
+ value = getattr(ctx, attr)
873
+ if value is not None:
874
+ span.set_attribute(f"mcp.context.{attr}", str(value))
875
+
876
+ # Add event for sampling start
877
+ span.add_event("sampling.request.started")
878
+
879
+ try:
880
+ result = await func(*args, **kwargs)
881
+ span.set_status(Status(StatusCode.OK))
882
+
883
+ # Add event for successful completion
884
+ span.add_event("sampling.request.completed")
885
+
886
+ # Capture result metadata
887
+ if result is not None and _detailed_tracing_enabled and isinstance(result, str):
888
+ span.set_attribute("mcp.sampling.result.content", _safe_serialize(result, 1000))
889
+
890
+ # Record metrics for successful sampling
891
+ try:
892
+ from golf.metrics import get_metrics_collector
893
+
894
+ metrics_collector = get_metrics_collector()
895
+ metrics_collector.increment_sampling(sampling_type, "success")
896
+ metrics_collector.record_sampling_duration(sampling_type, time.time() - start_time)
897
+ if isinstance(result, str):
898
+ metrics_collector.record_sampling_tokens(sampling_type, len(result.split()))
899
+ except ImportError:
900
+ pass
901
+
902
+ return result
903
+ except Exception as e:
904
+ span.record_exception(e)
905
+ span.set_status(Status(StatusCode.ERROR, str(e)))
906
+
907
+ # Add event for error
908
+ span.add_event(
909
+ "sampling.request.error",
910
+ {
911
+ "error.type": type(e).__name__,
912
+ "error.message": str(e),
913
+ },
914
+ )
915
+
916
+ # Record metrics for failed sampling
917
+ try:
918
+ from golf.metrics import get_metrics_collector
919
+
920
+ metrics_collector = get_metrics_collector()
921
+ metrics_collector.increment_sampling(sampling_type, "error")
922
+ metrics_collector.increment_error("sampling", type(e).__name__)
923
+ except ImportError:
924
+ pass
925
+
926
+ raise
927
+
928
+ @functools.wraps(func)
929
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
930
+ # If telemetry is disabled at runtime, call original function
931
+ global _provider
932
+ if _provider is None:
933
+ return func(*args, **kwargs)
934
+
935
+ # Record metrics timing
936
+ start_time = time.time()
937
+
938
+ # Create a more descriptive span name
939
+ span_name = f"mcp.sampling.{sampling_type}.request"
940
+ with tracer.start_as_current_span(span_name) as span:
941
+ # Add essential attributes
942
+ span.set_attribute("mcp.component.type", "sampling")
943
+ span.set_attribute("mcp.sampling.type", sampling_type)
944
+
945
+ # Add event for sampling start
946
+ span.add_event("sampling.request.started")
947
+
948
+ try:
949
+ result = func(*args, **kwargs)
950
+ span.set_status(Status(StatusCode.OK))
951
+
952
+ # Add event for successful completion
953
+ span.add_event("sampling.request.completed")
954
+
955
+ # Record metrics for successful sampling
956
+ try:
957
+ from golf.metrics import get_metrics_collector
958
+
959
+ metrics_collector = get_metrics_collector()
960
+ metrics_collector.increment_sampling(sampling_type, "success")
961
+ metrics_collector.record_sampling_duration(sampling_type, time.time() - start_time)
962
+ except ImportError:
963
+ pass
964
+
965
+ return result
966
+ except Exception as e:
967
+ span.record_exception(e)
968
+ span.set_status(Status(StatusCode.ERROR, str(e)))
969
+
970
+ # Add event for error
971
+ span.add_event(
972
+ "sampling.request.error",
973
+ {
974
+ "error.type": type(e).__name__,
975
+ "error.message": str(e),
976
+ },
977
+ )
978
+ raise
979
+
980
+ if asyncio.iscoroutinefunction(func):
981
+ return async_wrapper
982
+ else:
983
+ return sync_wrapper
984
+
985
+
625
986
  def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
626
987
  """Instrument a prompt function with OpenTelemetry tracing."""
627
988
  global _provider
@@ -633,20 +994,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
633
994
  tracer = get_tracer()
634
995
 
635
996
  @functools.wraps(func)
636
- async def async_wrapper(*args, **kwargs):
997
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
637
998
  # Create a more descriptive span name
638
999
  span_name = f"mcp.prompt.{prompt_name}.generate"
639
1000
  with tracer.start_as_current_span(span_name) as span:
640
- # Add comprehensive attributes
1001
+ # Add essential attributes only
641
1002
  span.set_attribute("mcp.component.type", "prompt")
642
- span.set_attribute("mcp.component.name", prompt_name)
643
1003
  span.set_attribute("mcp.prompt.name", prompt_name)
644
- span.set_attribute("mcp.prompt.function", func.__name__)
645
1004
  span.set_attribute(
646
1005
  "mcp.prompt.module",
647
1006
  func.__module__ if hasattr(func, "__module__") else "unknown",
648
1007
  )
649
- span.set_attribute("mcp.execution.async", True)
650
1008
 
651
1009
  # Extract Context parameter if present
652
1010
  ctx = kwargs.get("ctx")
@@ -678,9 +1036,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
678
1036
  span.set_status(Status(StatusCode.OK))
679
1037
 
680
1038
  # Add event for successful generation
681
- span.add_event(
682
- "prompt.generation.completed", {"prompt.name": prompt_name}
683
- )
1039
+ span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
684
1040
 
685
1041
  # Add message count and type information
686
1042
  if isinstance(result, list):
@@ -697,9 +1053,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
697
1053
 
698
1054
  if roles:
699
1055
  unique_roles = list(set(roles))
700
- span.set_attribute(
701
- "mcp.prompt.result.roles", ",".join(unique_roles)
702
- )
1056
+ span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
703
1057
  span.set_attribute(
704
1058
  "mcp.prompt.result.role_counts",
705
1059
  str({role: roles.count(role) for role in unique_roles}),
@@ -727,20 +1081,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
727
1081
  raise
728
1082
 
729
1083
  @functools.wraps(func)
730
- def sync_wrapper(*args, **kwargs):
1084
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
731
1085
  # Create a more descriptive span name
732
1086
  span_name = f"mcp.prompt.{prompt_name}.generate"
733
1087
  with tracer.start_as_current_span(span_name) as span:
734
- # Add comprehensive attributes
1088
+ # Add essential attributes only
735
1089
  span.set_attribute("mcp.component.type", "prompt")
736
- span.set_attribute("mcp.component.name", prompt_name)
737
1090
  span.set_attribute("mcp.prompt.name", prompt_name)
738
- span.set_attribute("mcp.prompt.function", func.__name__)
739
1091
  span.set_attribute(
740
1092
  "mcp.prompt.module",
741
1093
  func.__module__ if hasattr(func, "__module__") else "unknown",
742
1094
  )
743
- span.set_attribute("mcp.execution.async", False)
744
1095
 
745
1096
  # Extract Context parameter if present
746
1097
  ctx = kwargs.get("ctx")
@@ -772,9 +1123,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
772
1123
  span.set_status(Status(StatusCode.OK))
773
1124
 
774
1125
  # Add event for successful generation
775
- span.add_event(
776
- "prompt.generation.completed", {"prompt.name": prompt_name}
777
- )
1126
+ span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
778
1127
 
779
1128
  # Add message count and type information
780
1129
  if isinstance(result, list):
@@ -791,9 +1140,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
791
1140
 
792
1141
  if roles:
793
1142
  unique_roles = list(set(roles))
794
- span.set_attribute(
795
- "mcp.prompt.result.roles", ",".join(unique_roles)
796
- )
1143
+ span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
797
1144
  span.set_attribute(
798
1145
  "mcp.prompt.result.role_counts",
799
1146
  str({role: roles.count(role) for role in unique_roles}),
@@ -830,7 +1177,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
830
1177
  class BoundedSessionTracker:
831
1178
  """Memory-safe session tracker with automatic expiration."""
832
1179
 
833
- def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600):
1180
+ def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600) -> None:
834
1181
  self.max_sessions = max_sessions
835
1182
  self.session_ttl = session_ttl
836
1183
  self.sessions: OrderedDict[str, float] = OrderedDict()
@@ -860,13 +1207,9 @@ class BoundedSessionTracker:
860
1207
 
861
1208
  return True
862
1209
 
863
- def _cleanup_expired(self, current_time: float):
1210
+ def _cleanup_expired(self, current_time: float) -> None:
864
1211
  """Remove expired sessions."""
865
- expired = [
866
- sid
867
- for sid, timestamp in self.sessions.items()
868
- if current_time - timestamp > self.session_ttl
869
- ]
1212
+ expired = [sid for sid, timestamp in self.sessions.items() if current_time - timestamp > self.session_ttl]
870
1213
  for sid in expired:
871
1214
  del self.sessions[sid]
872
1215
 
@@ -875,14 +1218,12 @@ class BoundedSessionTracker:
875
1218
 
876
1219
 
877
1220
  class SessionTracingMiddleware(BaseHTTPMiddleware):
878
- def __init__(self, app):
1221
+ def __init__(self, app: Any) -> None:
879
1222
  super().__init__(app)
880
1223
  # Use memory-safe session tracker instead of unbounded collections
881
- self.session_tracker = BoundedSessionTracker(
882
- max_sessions=1000, session_ttl=3600
883
- )
1224
+ self.session_tracker = BoundedSessionTracker(max_sessions=1000, session_ttl=3600)
884
1225
 
885
- async def dispatch(self, request: Request, call_next):
1226
+ async def dispatch(self, request: Any, call_next: Callable[..., Any]) -> Any:
886
1227
  # Record HTTP request timing
887
1228
  import time
888
1229
 
@@ -912,7 +1253,8 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
912
1253
  from golf.metrics import get_metrics_collector
913
1254
 
914
1255
  metrics_collector = get_metrics_collector()
915
- # Use a default duration since we don't track exact start times anymore
1256
+ # Use a default duration since we don't track exact start
1257
+ # times anymore
916
1258
  # This is less precise but memory-safe
917
1259
  metrics_collector.record_session_duration(300.0) # 5 min default
918
1260
  except ImportError:
@@ -935,15 +1277,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
935
1277
 
936
1278
  tracer = get_tracer()
937
1279
  with tracer.start_as_current_span(span_name) as span:
938
- # Add comprehensive HTTP attributes
1280
+ # Add essential HTTP attributes
939
1281
  span.set_attribute("http.method", method)
940
- span.set_attribute("http.url", str(request.url))
941
- span.set_attribute("http.scheme", request.url.scheme)
942
- span.set_attribute("http.host", request.url.hostname or "unknown")
943
1282
  span.set_attribute("http.target", path)
944
- span.set_attribute(
945
- "http.user_agent", request.headers.get("user-agent", "unknown")
946
- )
1283
+ span.set_attribute("http.host", request.url.hostname or "unknown")
947
1284
 
948
1285
  # Add session tracking
949
1286
  if session_id:
@@ -973,15 +1310,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
973
1310
 
974
1311
  # Add response attributes
975
1312
  span.set_attribute("http.status_code", response.status_code)
976
- span.set_attribute(
977
- "http.status_class", f"{response.status_code // 100}xx"
978
- )
979
1313
 
980
1314
  # Set span status based on HTTP status
981
1315
  if response.status_code >= 400:
982
- span.set_status(
983
- Status(StatusCode.ERROR, f"HTTP {response.status_code}")
984
- )
1316
+ span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
985
1317
  else:
986
1318
  span.set_status(Status(StatusCode.OK))
987
1319
 
@@ -1004,16 +1336,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
1004
1336
  # Clean up path for metrics (remove query params, normalize)
1005
1337
  clean_path = path.split("?")[0] # Remove query parameters
1006
1338
  if clean_path.startswith("/"):
1007
- clean_path = (
1008
- clean_path[1:] or "root"
1009
- ) # Remove leading slash, handle root
1010
-
1011
- metrics_collector.increment_http_request(
1012
- method, response.status_code, clean_path
1013
- )
1014
- metrics_collector.record_http_duration(
1015
- method, clean_path, time.time() - start_time
1016
- )
1339
+ clean_path = clean_path[1:] or "root" # Remove leading slash, handle root
1340
+
1341
+ metrics_collector.increment_http_request(method, response.status_code, clean_path)
1342
+ metrics_collector.record_http_duration(method, clean_path, time.time() - start_time)
1017
1343
  except ImportError:
1018
1344
  # Metrics not available, continue without metrics
1019
1345
  pass
@@ -1045,9 +1371,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
1045
1371
  if clean_path.startswith("/"):
1046
1372
  clean_path = clean_path[1:] or "root"
1047
1373
 
1048
- metrics_collector.increment_http_request(
1049
- method, 500, clean_path
1050
- ) # Assume 500 for exceptions
1374
+ metrics_collector.increment_http_request(method, 500, clean_path) # Assume 500 for exceptions
1051
1375
  metrics_collector.increment_error("http", type(e).__name__)
1052
1376
  except ImportError:
1053
1377
  pass
@@ -1059,7 +1383,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
1059
1383
 
1060
1384
 
1061
1385
  @asynccontextmanager
1062
- async def telemetry_lifespan(mcp_instance):
1386
+ async def telemetry_lifespan(mcp_instance: Any) -> AsyncGenerator[None, None]:
1063
1387
  """Simplified lifespan for telemetry initialization and cleanup."""
1064
1388
  global _provider
1065
1389
 
@@ -1081,9 +1405,7 @@ async def telemetry_lifespan(mcp_instance):
1081
1405
  app.add_middleware(SessionTracingMiddleware)
1082
1406
 
1083
1407
  # Also try to instrument FastMCP's internal handlers
1084
- if hasattr(mcp_instance, "_tool_manager") and hasattr(
1085
- mcp_instance._tool_manager, "tools"
1086
- ):
1408
+ if hasattr(mcp_instance, "_tool_manager") and hasattr(mcp_instance._tool_manager, "tools"):
1087
1409
  # The tools should already be instrumented when they were registered
1088
1410
  pass
1089
1411
 
@@ -1091,7 +1413,7 @@ async def telemetry_lifespan(mcp_instance):
1091
1413
  if hasattr(mcp_instance, "handle_request"):
1092
1414
  original_handle_request = mcp_instance.handle_request
1093
1415
 
1094
- async def traced_handle_request(*args, **kwargs):
1416
+ async def traced_handle_request(*args: Any, **kwargs: Any) -> Any:
1095
1417
  tracer = get_tracer()
1096
1418
  with tracer.start_as_current_span("mcp.handle_request") as span:
1097
1419
  span.set_attribute("mcp.request.handler", "handle_request")