golf-mcp 0.1.20__py3-none-any.whl → 0.2.1__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 +235 -83
  5. golf/auth/api_key.py +6 -14
  6. golf/auth/factory.py +358 -0
  7. golf/auth/helpers.py +12 -42
  8. golf/auth/providers.py +446 -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 +355 -414
  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 +484 -178
  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.20.dist-info → golf_mcp-0.2.1.dist-info}/METADATA +51 -104
  99. golf_mcp-0.2.1.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.20.dist-info/RECORD +0 -60
  120. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/WHEEL +0 -0
  121. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/entry_points.txt +0 -0
  122. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/licenses/LICENSE +0 -0
  123. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.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:
@@ -37,27 +72,24 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
37
72
  # Check for Golf platform integration first
38
73
  golf_api_key = os.environ.get("GOLF_API_KEY")
39
74
  if golf_api_key:
40
- # Auto-configure for Golf platform - always use OTLP when Golf API key is present
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
-
78
+
43
79
  # Only set endpoint if not already configured (allow user override)
44
80
  if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
45
- os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = (
46
- "https://golf-backend.golf-auth-1.authed-qukc4.ryvn.run/api/v1/otel"
47
- )
48
-
81
+ os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = _endpoints.OTEL_ENDPOINT
82
+
49
83
  # Set Golf platform headers (append to existing if present)
50
84
  existing_headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
51
85
  golf_header = f"X-Golf-Key={golf_api_key}"
52
-
86
+
53
87
  if existing_headers:
54
88
  # Check if Golf key is already in headers
55
89
  if "X-Golf-Key=" not in existing_headers:
56
90
  os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"{existing_headers},{golf_header}"
57
91
  else:
58
92
  os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = golf_header
59
-
60
- print("[INFO] Auto-configured OpenTelemetry for Golf platform ingestion")
61
93
 
62
94
  # Check for required environment variables based on exporter type
63
95
  exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
@@ -94,9 +126,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
94
126
  # Configure exporter based on type
95
127
  try:
96
128
  if exporter_type == "otlp_http":
97
- endpoint = os.environ.get(
98
- "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
99
- )
129
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
100
130
  headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
101
131
 
102
132
  # Parse headers if provided
@@ -107,13 +137,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
107
137
  key, value = header.split("=", 1)
108
138
  header_dict[key.strip()] = value.strip()
109
139
 
110
- exporter = OTLPSpanExporter(
111
- endpoint=endpoint, headers=header_dict if header_dict else None
112
- )
140
+ exporter = OTLPSpanExporter(endpoint=endpoint, headers=header_dict if header_dict else None)
113
141
 
114
- # Log successful configuration for Golf platform
115
- if golf_api_key:
116
- print(f"[INFO] OpenTelemetry configured for Golf platform: {endpoint}")
117
142
  else:
118
143
  # Default to console exporter
119
144
  exporter = ConsoleSpanExporter(out=sys.stderr)
@@ -128,7 +153,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
128
153
  processor = BatchSpanProcessor(
129
154
  exporter,
130
155
  max_queue_size=2048,
131
- 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
132
158
  max_export_batch_size=512,
133
159
  export_timeout_millis=5000,
134
160
  )
@@ -143,10 +169,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
143
169
  try:
144
170
  # Check if a provider is already set to avoid the warning
145
171
  existing_provider = trace.get_tracer_provider()
146
- if (
147
- existing_provider is None
148
- or str(type(existing_provider).__name__) == "ProxyTracerProvider"
149
- ):
172
+ if existing_provider is None or str(type(existing_provider).__name__) == "ProxyTracerProvider":
150
173
  # Only set if no provider exists or it's the default proxy provider
151
174
  trace.set_tracer_provider(provider)
152
175
  _provider = provider
@@ -183,7 +206,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
183
206
  tracer = get_tracer()
184
207
 
185
208
  @functools.wraps(func)
186
- async def async_wrapper(*args, **kwargs):
209
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
187
210
  # Record metrics timing
188
211
  import time
189
212
 
@@ -194,20 +217,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
194
217
 
195
218
  # start_as_current_span automatically uses the current context and manages it
196
219
  with tracer.start_as_current_span(span_name) as span:
197
- # Add comprehensive attributes
220
+ # Add essential attributes only
198
221
  span.set_attribute("mcp.component.type", "tool")
199
- span.set_attribute("mcp.component.name", tool_name)
200
222
  span.set_attribute("mcp.tool.name", tool_name)
201
- span.set_attribute("mcp.tool.function", func.__name__)
202
223
  span.set_attribute(
203
224
  "mcp.tool.module",
204
225
  func.__module__ if hasattr(func, "__module__") else "unknown",
205
226
  )
206
227
 
207
- # Add execution context
208
- span.set_attribute("mcp.execution.args_count", len(args))
209
- span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
210
- 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)
211
239
 
212
240
  # Extract Context parameter if present
213
241
  ctx = kwargs.get("ctx")
@@ -247,42 +275,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
247
275
 
248
276
  metrics_collector = get_metrics_collector()
249
277
  metrics_collector.increment_tool_execution(tool_name, "success")
250
- metrics_collector.record_tool_duration(
251
- tool_name, time.time() - start_time
252
- )
278
+ metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
253
279
  except ImportError:
254
280
  # Metrics not available, continue without metrics
255
281
  pass
256
282
 
257
- # Capture result metadata with better structure
283
+ # Capture result metadata
258
284
  if result is not None:
259
- if isinstance(result, str | int | float | bool):
260
- span.set_attribute("mcp.tool.result.value", str(result))
261
- span.set_attribute(
262
- "mcp.tool.result.type", type(result).__name__
263
- )
264
- elif isinstance(result, list):
265
- span.set_attribute("mcp.tool.result.count", len(result))
266
- span.set_attribute("mcp.tool.result.type", "array")
267
- elif isinstance(result, dict):
268
- span.set_attribute("mcp.tool.result.count", len(result))
269
- span.set_attribute("mcp.tool.result.type", "object")
270
- # Only show first few keys to avoid exceeding attribute limits
271
- if len(result) > 0 and len(result) <= 5:
272
- keys_list = list(result.keys())[:5]
273
- # Limit key length and join
274
- truncated_keys = [
275
- str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
276
- for k in keys_list
277
- ]
278
- span.set_attribute(
279
- "mcp.tool.result.sample_keys", ",".join(truncated_keys)
280
- )
281
- 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):
282
290
  span.set_attribute("mcp.tool.result.length", len(result))
283
291
 
284
- # For any result, record its type
285
- 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)
286
297
 
287
298
  return result
288
299
  except Exception as e:
@@ -313,7 +324,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
313
324
  raise
314
325
 
315
326
  @functools.wraps(func)
316
- def sync_wrapper(*args, **kwargs):
327
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
317
328
  # Record metrics timing
318
329
  import time
319
330
 
@@ -324,11 +335,9 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
324
335
 
325
336
  # start_as_current_span automatically uses the current context and manages it
326
337
  with tracer.start_as_current_span(span_name) as span:
327
- # Add comprehensive attributes
338
+ # Add essential attributes only
328
339
  span.set_attribute("mcp.component.type", "tool")
329
- span.set_attribute("mcp.component.name", tool_name)
330
340
  span.set_attribute("mcp.tool.name", tool_name)
331
- span.set_attribute("mcp.tool.function", func.__name__)
332
341
  span.set_attribute(
333
342
  "mcp.tool.module",
334
343
  func.__module__ if hasattr(func, "__module__") else "unknown",
@@ -337,7 +346,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
337
346
  # Add execution context
338
347
  span.set_attribute("mcp.execution.args_count", len(args))
339
348
  span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
340
- span.set_attribute("mcp.execution.async", False)
341
349
 
342
350
  # Extract Context parameter if present
343
351
  ctx = kwargs.get("ctx")
@@ -377,42 +385,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
377
385
 
378
386
  metrics_collector = get_metrics_collector()
379
387
  metrics_collector.increment_tool_execution(tool_name, "success")
380
- metrics_collector.record_tool_duration(
381
- tool_name, time.time() - start_time
382
- )
388
+ metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
383
389
  except ImportError:
384
390
  # Metrics not available, continue without metrics
385
391
  pass
386
392
 
387
- # Capture result metadata with better structure
393
+ # Capture result metadata
388
394
  if result is not None:
389
- if isinstance(result, str | int | float | bool):
390
- span.set_attribute("mcp.tool.result.value", str(result))
391
- span.set_attribute(
392
- "mcp.tool.result.type", type(result).__name__
393
- )
394
- elif isinstance(result, list):
395
- span.set_attribute("mcp.tool.result.count", len(result))
396
- span.set_attribute("mcp.tool.result.type", "array")
397
- elif isinstance(result, dict):
398
- span.set_attribute("mcp.tool.result.count", len(result))
399
- span.set_attribute("mcp.tool.result.type", "object")
400
- # Only show first few keys to avoid exceeding attribute limits
401
- if len(result) > 0 and len(result) <= 5:
402
- keys_list = list(result.keys())[:5]
403
- # Limit key length and join
404
- truncated_keys = [
405
- str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
406
- for k in keys_list
407
- ]
408
- span.set_attribute(
409
- "mcp.tool.result.sample_keys", ",".join(truncated_keys)
410
- )
411
- 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):
412
400
  span.set_attribute("mcp.tool.result.length", len(result))
413
401
 
414
- # For any result, record its type
415
- 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)
416
407
 
417
408
  return result
418
409
  except Exception as e:
@@ -463,21 +454,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
463
454
  is_template = "{" in resource_uri
464
455
 
465
456
  @functools.wraps(func)
466
- async def async_wrapper(*args, **kwargs):
457
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
467
458
  # Create a more descriptive span name
468
459
  span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
469
460
  with tracer.start_as_current_span(span_name) as span:
470
- # Add comprehensive attributes
461
+ # Add essential attributes only
471
462
  span.set_attribute("mcp.component.type", "resource")
472
- span.set_attribute("mcp.component.name", resource_uri)
473
463
  span.set_attribute("mcp.resource.uri", resource_uri)
474
464
  span.set_attribute("mcp.resource.is_template", is_template)
475
- span.set_attribute("mcp.resource.function", func.__name__)
476
465
  span.set_attribute(
477
466
  "mcp.resource.module",
478
467
  func.__module__ if hasattr(func, "__module__") else "unknown",
479
468
  )
480
- span.set_attribute("mcp.execution.async", True)
481
469
 
482
470
  # Extract Context parameter if present
483
471
  ctx = kwargs.get("ctx")
@@ -509,9 +497,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
509
497
  span.set_status(Status(StatusCode.OK))
510
498
 
511
499
  # Add event for successful read
512
- span.add_event(
513
- "resource.read.completed", {"resource.uri": resource_uri}
514
- )
500
+ span.add_event("resource.read.completed", {"resource.uri": resource_uri})
515
501
 
516
502
  # Add result metadata
517
503
  if hasattr(result, "__len__"):
@@ -548,21 +534,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
548
534
  raise
549
535
 
550
536
  @functools.wraps(func)
551
- def sync_wrapper(*args, **kwargs):
537
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
552
538
  # Create a more descriptive span name
553
539
  span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
554
540
  with tracer.start_as_current_span(span_name) as span:
555
- # Add comprehensive attributes
541
+ # Add essential attributes only
556
542
  span.set_attribute("mcp.component.type", "resource")
557
- span.set_attribute("mcp.component.name", resource_uri)
558
543
  span.set_attribute("mcp.resource.uri", resource_uri)
559
544
  span.set_attribute("mcp.resource.is_template", is_template)
560
- span.set_attribute("mcp.resource.function", func.__name__)
561
545
  span.set_attribute(
562
546
  "mcp.resource.module",
563
547
  func.__module__ if hasattr(func, "__module__") else "unknown",
564
548
  )
565
- span.set_attribute("mcp.execution.async", False)
566
549
 
567
550
  # Extract Context parameter if present
568
551
  ctx = kwargs.get("ctx")
@@ -594,9 +577,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
594
577
  span.set_status(Status(StatusCode.OK))
595
578
 
596
579
  # Add event for successful read
597
- span.add_event(
598
- "resource.read.completed", {"resource.uri": resource_uri}
599
- )
580
+ span.add_event("resource.read.completed", {"resource.uri": resource_uri})
600
581
 
601
582
  # Add result metadata
602
583
  if hasattr(result, "__len__"):
@@ -638,6 +619,370 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
638
619
  return sync_wrapper
639
620
 
640
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
+
641
986
  def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
642
987
  """Instrument a prompt function with OpenTelemetry tracing."""
643
988
  global _provider
@@ -649,20 +994,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
649
994
  tracer = get_tracer()
650
995
 
651
996
  @functools.wraps(func)
652
- async def async_wrapper(*args, **kwargs):
997
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
653
998
  # Create a more descriptive span name
654
999
  span_name = f"mcp.prompt.{prompt_name}.generate"
655
1000
  with tracer.start_as_current_span(span_name) as span:
656
- # Add comprehensive attributes
1001
+ # Add essential attributes only
657
1002
  span.set_attribute("mcp.component.type", "prompt")
658
- span.set_attribute("mcp.component.name", prompt_name)
659
1003
  span.set_attribute("mcp.prompt.name", prompt_name)
660
- span.set_attribute("mcp.prompt.function", func.__name__)
661
1004
  span.set_attribute(
662
1005
  "mcp.prompt.module",
663
1006
  func.__module__ if hasattr(func, "__module__") else "unknown",
664
1007
  )
665
- span.set_attribute("mcp.execution.async", True)
666
1008
 
667
1009
  # Extract Context parameter if present
668
1010
  ctx = kwargs.get("ctx")
@@ -694,9 +1036,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
694
1036
  span.set_status(Status(StatusCode.OK))
695
1037
 
696
1038
  # Add event for successful generation
697
- span.add_event(
698
- "prompt.generation.completed", {"prompt.name": prompt_name}
699
- )
1039
+ span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
700
1040
 
701
1041
  # Add message count and type information
702
1042
  if isinstance(result, list):
@@ -713,9 +1053,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
713
1053
 
714
1054
  if roles:
715
1055
  unique_roles = list(set(roles))
716
- span.set_attribute(
717
- "mcp.prompt.result.roles", ",".join(unique_roles)
718
- )
1056
+ span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
719
1057
  span.set_attribute(
720
1058
  "mcp.prompt.result.role_counts",
721
1059
  str({role: roles.count(role) for role in unique_roles}),
@@ -743,20 +1081,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
743
1081
  raise
744
1082
 
745
1083
  @functools.wraps(func)
746
- def sync_wrapper(*args, **kwargs):
1084
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
747
1085
  # Create a more descriptive span name
748
1086
  span_name = f"mcp.prompt.{prompt_name}.generate"
749
1087
  with tracer.start_as_current_span(span_name) as span:
750
- # Add comprehensive attributes
1088
+ # Add essential attributes only
751
1089
  span.set_attribute("mcp.component.type", "prompt")
752
- span.set_attribute("mcp.component.name", prompt_name)
753
1090
  span.set_attribute("mcp.prompt.name", prompt_name)
754
- span.set_attribute("mcp.prompt.function", func.__name__)
755
1091
  span.set_attribute(
756
1092
  "mcp.prompt.module",
757
1093
  func.__module__ if hasattr(func, "__module__") else "unknown",
758
1094
  )
759
- span.set_attribute("mcp.execution.async", False)
760
1095
 
761
1096
  # Extract Context parameter if present
762
1097
  ctx = kwargs.get("ctx")
@@ -788,9 +1123,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
788
1123
  span.set_status(Status(StatusCode.OK))
789
1124
 
790
1125
  # Add event for successful generation
791
- span.add_event(
792
- "prompt.generation.completed", {"prompt.name": prompt_name}
793
- )
1126
+ span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
794
1127
 
795
1128
  # Add message count and type information
796
1129
  if isinstance(result, list):
@@ -807,9 +1140,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
807
1140
 
808
1141
  if roles:
809
1142
  unique_roles = list(set(roles))
810
- span.set_attribute(
811
- "mcp.prompt.result.roles", ",".join(unique_roles)
812
- )
1143
+ span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
813
1144
  span.set_attribute(
814
1145
  "mcp.prompt.result.role_counts",
815
1146
  str({role: roles.count(role) for role in unique_roles}),
@@ -846,7 +1177,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
846
1177
  class BoundedSessionTracker:
847
1178
  """Memory-safe session tracker with automatic expiration."""
848
1179
 
849
- def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600):
1180
+ def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600) -> None:
850
1181
  self.max_sessions = max_sessions
851
1182
  self.session_ttl = session_ttl
852
1183
  self.sessions: OrderedDict[str, float] = OrderedDict()
@@ -876,13 +1207,9 @@ class BoundedSessionTracker:
876
1207
 
877
1208
  return True
878
1209
 
879
- def _cleanup_expired(self, current_time: float):
1210
+ def _cleanup_expired(self, current_time: float) -> None:
880
1211
  """Remove expired sessions."""
881
- expired = [
882
- sid
883
- for sid, timestamp in self.sessions.items()
884
- if current_time - timestamp > self.session_ttl
885
- ]
1212
+ expired = [sid for sid, timestamp in self.sessions.items() if current_time - timestamp > self.session_ttl]
886
1213
  for sid in expired:
887
1214
  del self.sessions[sid]
888
1215
 
@@ -891,14 +1218,12 @@ class BoundedSessionTracker:
891
1218
 
892
1219
 
893
1220
  class SessionTracingMiddleware(BaseHTTPMiddleware):
894
- def __init__(self, app):
1221
+ def __init__(self, app: Any) -> None:
895
1222
  super().__init__(app)
896
1223
  # Use memory-safe session tracker instead of unbounded collections
897
- self.session_tracker = BoundedSessionTracker(
898
- max_sessions=1000, session_ttl=3600
899
- )
1224
+ self.session_tracker = BoundedSessionTracker(max_sessions=1000, session_ttl=3600)
900
1225
 
901
- async def dispatch(self, request: Request, call_next):
1226
+ async def dispatch(self, request: Any, call_next: Callable[..., Any]) -> Any:
902
1227
  # Record HTTP request timing
903
1228
  import time
904
1229
 
@@ -928,7 +1253,8 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
928
1253
  from golf.metrics import get_metrics_collector
929
1254
 
930
1255
  metrics_collector = get_metrics_collector()
931
- # 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
932
1258
  # This is less precise but memory-safe
933
1259
  metrics_collector.record_session_duration(300.0) # 5 min default
934
1260
  except ImportError:
@@ -951,15 +1277,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
951
1277
 
952
1278
  tracer = get_tracer()
953
1279
  with tracer.start_as_current_span(span_name) as span:
954
- # Add comprehensive HTTP attributes
1280
+ # Add essential HTTP attributes
955
1281
  span.set_attribute("http.method", method)
956
- span.set_attribute("http.url", str(request.url))
957
- span.set_attribute("http.scheme", request.url.scheme)
958
- span.set_attribute("http.host", request.url.hostname or "unknown")
959
1282
  span.set_attribute("http.target", path)
960
- span.set_attribute(
961
- "http.user_agent", request.headers.get("user-agent", "unknown")
962
- )
1283
+ span.set_attribute("http.host", request.url.hostname or "unknown")
963
1284
 
964
1285
  # Add session tracking
965
1286
  if session_id:
@@ -989,15 +1310,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
989
1310
 
990
1311
  # Add response attributes
991
1312
  span.set_attribute("http.status_code", response.status_code)
992
- span.set_attribute(
993
- "http.status_class", f"{response.status_code // 100}xx"
994
- )
995
1313
 
996
1314
  # Set span status based on HTTP status
997
1315
  if response.status_code >= 400:
998
- span.set_status(
999
- Status(StatusCode.ERROR, f"HTTP {response.status_code}")
1000
- )
1316
+ span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
1001
1317
  else:
1002
1318
  span.set_status(Status(StatusCode.OK))
1003
1319
 
@@ -1020,16 +1336,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
1020
1336
  # Clean up path for metrics (remove query params, normalize)
1021
1337
  clean_path = path.split("?")[0] # Remove query parameters
1022
1338
  if clean_path.startswith("/"):
1023
- clean_path = (
1024
- clean_path[1:] or "root"
1025
- ) # Remove leading slash, handle root
1026
-
1027
- metrics_collector.increment_http_request(
1028
- method, response.status_code, clean_path
1029
- )
1030
- metrics_collector.record_http_duration(
1031
- method, clean_path, time.time() - start_time
1032
- )
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)
1033
1343
  except ImportError:
1034
1344
  # Metrics not available, continue without metrics
1035
1345
  pass
@@ -1061,9 +1371,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
1061
1371
  if clean_path.startswith("/"):
1062
1372
  clean_path = clean_path[1:] or "root"
1063
1373
 
1064
- metrics_collector.increment_http_request(
1065
- method, 500, clean_path
1066
- ) # Assume 500 for exceptions
1374
+ metrics_collector.increment_http_request(method, 500, clean_path) # Assume 500 for exceptions
1067
1375
  metrics_collector.increment_error("http", type(e).__name__)
1068
1376
  except ImportError:
1069
1377
  pass
@@ -1075,7 +1383,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
1075
1383
 
1076
1384
 
1077
1385
  @asynccontextmanager
1078
- async def telemetry_lifespan(mcp_instance):
1386
+ async def telemetry_lifespan(mcp_instance: Any) -> AsyncGenerator[None, None]:
1079
1387
  """Simplified lifespan for telemetry initialization and cleanup."""
1080
1388
  global _provider
1081
1389
 
@@ -1097,9 +1405,7 @@ async def telemetry_lifespan(mcp_instance):
1097
1405
  app.add_middleware(SessionTracingMiddleware)
1098
1406
 
1099
1407
  # Also try to instrument FastMCP's internal handlers
1100
- if hasattr(mcp_instance, "_tool_manager") and hasattr(
1101
- mcp_instance._tool_manager, "tools"
1102
- ):
1408
+ if hasattr(mcp_instance, "_tool_manager") and hasattr(mcp_instance._tool_manager, "tools"):
1103
1409
  # The tools should already be instrumented when they were registered
1104
1410
  pass
1105
1411
 
@@ -1107,7 +1413,7 @@ async def telemetry_lifespan(mcp_instance):
1107
1413
  if hasattr(mcp_instance, "handle_request"):
1108
1414
  original_handle_request = mcp_instance.handle_request
1109
1415
 
1110
- async def traced_handle_request(*args, **kwargs):
1416
+ async def traced_handle_request(*args: Any, **kwargs: Any) -> Any:
1111
1417
  tracer = get_tracer()
1112
1418
  with tracer.start_as_current_span("mcp.handle_request") as span:
1113
1419
  span.set_attribute("mcp.request.handler", "handle_request")