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

golf/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.17"
1
+ __version__ = "0.1.19"
golf/core/builder.py CHANGED
@@ -562,6 +562,18 @@ class CodeGenerator:
562
562
  if self.settings.opentelemetry_enabled:
563
563
  imports.extend(generate_telemetry_imports())
564
564
 
565
+ # Add metrics imports if enabled
566
+ if self.settings.metrics_enabled:
567
+ from golf.core.builder_metrics import (
568
+ generate_metrics_imports,
569
+ generate_metrics_instrumentation,
570
+ generate_session_tracking,
571
+ )
572
+
573
+ imports.extend(generate_metrics_imports())
574
+ imports.extend(generate_metrics_instrumentation())
575
+ imports.extend(generate_session_tracking())
576
+
565
577
  # Add health check imports if enabled
566
578
  if self.settings.health_check_enabled:
567
579
  imports.extend(
@@ -672,6 +684,44 @@ class CodeGenerator:
672
684
  f"{full_module_path}.{entry_func}, '{component.name}')"
673
685
  )
674
686
 
687
+ if component_type == ComponentType.TOOL:
688
+ registration += (
689
+ f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
690
+ f'description="{component.docstring or ""}"'
691
+ )
692
+ # Add annotations if present
693
+ if hasattr(component, "annotations") and component.annotations:
694
+ registration += f", annotations={component.annotations}"
695
+ registration += ")"
696
+ elif component_type == ComponentType.RESOURCE:
697
+ registration += (
698
+ f"\nmcp.add_resource_fn(_wrapped_func, "
699
+ f'uri="{component.uri_template}", name="{component.name}", '
700
+ f'description="{component.docstring or ""}")'
701
+ )
702
+ else: # PROMPT
703
+ registration += (
704
+ f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
705
+ f'description="{component.docstring or ""}")'
706
+ )
707
+ elif self.settings.metrics_enabled:
708
+ # Use metrics instrumentation
709
+ registration = (
710
+ f"# Register the {component_type.value} "
711
+ f"'{component.name}' with metrics"
712
+ )
713
+ entry_func = (
714
+ component.entry_function
715
+ if hasattr(component, "entry_function")
716
+ and component.entry_function
717
+ else "export"
718
+ )
719
+
720
+ registration += (
721
+ f"\n_wrapped_func = instrument_{component_type.value}("
722
+ f"{full_module_path}.{entry_func}, '{component.name}')"
723
+ )
724
+
675
725
  if component_type == ComponentType.TOOL:
676
726
  registration += (
677
727
  f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
@@ -826,6 +876,15 @@ class CodeGenerator:
826
876
  ]
827
877
  )
828
878
 
879
+ # Add metrics initialization if enabled
880
+ early_metrics_init = []
881
+ if self.settings.metrics_enabled:
882
+ from golf.core.builder_metrics import generate_metrics_initialization
883
+
884
+ early_metrics_init.extend(
885
+ generate_metrics_initialization(self.settings.name)
886
+ )
887
+
829
888
  # Main entry point with transport-specific app initialization
830
889
  main_code = [
831
890
  'if __name__ == "__main__":',
@@ -864,6 +923,13 @@ class CodeGenerator:
864
923
  )
865
924
  middleware_list.append("Middleware(ApiKeyMiddleware)")
866
925
 
926
+ # Add metrics middleware if enabled
927
+ if self.settings.metrics_enabled:
928
+ middleware_setup.append(
929
+ " from starlette.middleware import Middleware"
930
+ )
931
+ middleware_list.append("Middleware(MetricsMiddleware)")
932
+
867
933
  # Add OpenTelemetry middleware if enabled
868
934
  if self.settings.opentelemetry_enabled:
869
935
  middleware_setup.append(
@@ -904,6 +970,13 @@ class CodeGenerator:
904
970
  )
905
971
  middleware_list.append("Middleware(ApiKeyMiddleware)")
906
972
 
973
+ # Add metrics middleware if enabled
974
+ if self.settings.metrics_enabled:
975
+ middleware_setup.append(
976
+ " from starlette.middleware import Middleware"
977
+ )
978
+ middleware_list.append("Middleware(MetricsMiddleware)")
979
+
907
980
  # Add OpenTelemetry middleware if enabled
908
981
  if self.settings.opentelemetry_enabled:
909
982
  middleware_setup.append(
@@ -937,6 +1010,13 @@ class CodeGenerator:
937
1010
  [" # Run with stdio transport", ' mcp.run(transport="stdio")']
938
1011
  )
939
1012
 
1013
+ # Add metrics route if enabled
1014
+ metrics_route_code = []
1015
+ if self.settings.metrics_enabled:
1016
+ from golf.core.builder_metrics import generate_metrics_route
1017
+
1018
+ metrics_route_code = generate_metrics_route(self.settings.metrics_path)
1019
+
940
1020
  # Add health check route if enabled
941
1021
  health_check_code = []
942
1022
  if self.settings.health_check_enabled:
@@ -953,14 +1033,16 @@ class CodeGenerator:
953
1033
 
954
1034
  # Combine all sections
955
1035
  # Order: imports, env_section, auth_setup, server_code (mcp init),
956
- # early_telemetry_init, component_registrations, health_check_code, main_code (run block)
1036
+ # early_telemetry_init, early_metrics_init, component_registrations, metrics_route_code, health_check_code, main_code (run block)
957
1037
  code = "\n".join(
958
1038
  imports
959
1039
  + env_section
960
1040
  + auth_setup_code
961
1041
  + server_code_lines
962
1042
  + early_telemetry_init
1043
+ + early_metrics_init
963
1044
  + component_registrations
1045
+ + metrics_route_code
964
1046
  + health_check_code
965
1047
  + main_code
966
1048
  )
golf/core/builder_auth.py CHANGED
@@ -187,6 +187,11 @@ def generate_api_key_auth_components(
187
187
  " # Debug mode from environment",
188
188
  " debug = os.environ.get('GOLF_API_KEY_DEBUG', '').lower() == 'true'",
189
189
  " ",
190
+ " # Skip auth for monitoring endpoints",
191
+ " path = request.url.path",
192
+ " if path in ['/metrics', '/health']:",
193
+ " return await call_next(request)",
194
+ " ",
190
195
  " api_key_config = get_api_key_config()",
191
196
  " ",
192
197
  " if api_key_config:",
@@ -0,0 +1,232 @@
1
+ """Metrics integration for the GolfMCP build process.
2
+
3
+ This module provides functions for generating Prometheus metrics initialization
4
+ and collection code for FastMCP servers built with GolfMCP.
5
+ """
6
+
7
+
8
+ def generate_metrics_imports() -> list[str]:
9
+ """Generate import statements for metrics collection.
10
+
11
+ Returns:
12
+ List of import statements for metrics
13
+ """
14
+ return [
15
+ "# Prometheus metrics imports",
16
+ "from golf.metrics import init_metrics, get_metrics_collector",
17
+ "from prometheus_client import generate_latest, CONTENT_TYPE_LATEST",
18
+ "from starlette.responses import Response",
19
+ "from starlette.middleware.base import BaseHTTPMiddleware",
20
+ "from starlette.requests import Request",
21
+ "import time",
22
+ ]
23
+
24
+
25
+ def generate_metrics_initialization(server_name: str) -> list[str]:
26
+ """Generate metrics initialization code.
27
+
28
+ Args:
29
+ server_name: Name of the MCP server
30
+
31
+ Returns:
32
+ List of code lines for metrics initialization
33
+ """
34
+ return [
35
+ "# Initialize metrics collection",
36
+ "init_metrics(enabled=True)",
37
+ "",
38
+ ]
39
+
40
+
41
+ def generate_metrics_route(metrics_path: str) -> list[str]:
42
+ """Generate the metrics endpoint route code.
43
+
44
+ Args:
45
+ metrics_path: Path for the metrics endpoint (e.g., "/metrics")
46
+
47
+ Returns:
48
+ List of code lines for the metrics route
49
+ """
50
+ return [
51
+ "# Add metrics endpoint",
52
+ f'@mcp.custom_route("{metrics_path}", methods=["GET"])',
53
+ "async def metrics_endpoint(request):",
54
+ ' """Prometheus metrics endpoint for monitoring."""',
55
+ " # Update uptime before returning metrics",
56
+ " update_uptime()",
57
+ " return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)",
58
+ "",
59
+ ]
60
+
61
+
62
+ def get_metrics_dependencies() -> list[str]:
63
+ """Get list of metrics dependencies to add to pyproject.toml.
64
+
65
+ Returns:
66
+ List of package requirements strings
67
+ """
68
+ return [
69
+ "prometheus-client>=0.19.0",
70
+ ]
71
+
72
+
73
+ def generate_metrics_instrumentation() -> list[str]:
74
+ """Generate metrics instrumentation wrapper functions.
75
+
76
+ Returns:
77
+ List of code lines for metrics instrumentation
78
+ """
79
+ return [
80
+ "# Metrics instrumentation wrapper functions",
81
+ "import time",
82
+ "import functools",
83
+ "from typing import Any, Callable",
84
+ "",
85
+ "def instrument_tool(func: Callable, tool_name: str) -> Callable:",
86
+ ' """Wrap a tool function with metrics collection."""',
87
+ " @functools.wraps(func)",
88
+ " async def wrapper(*args, **kwargs) -> Any:",
89
+ " collector = get_metrics_collector()",
90
+ " start_time = time.time()",
91
+ " status = 'success'",
92
+ " try:",
93
+ " result = await func(*args, **kwargs)",
94
+ " return result",
95
+ " except Exception as e:",
96
+ " status = 'error'",
97
+ " collector.increment_error('tool', type(e).__name__)",
98
+ " raise",
99
+ " finally:",
100
+ " duration = time.time() - start_time",
101
+ " collector.increment_tool_execution(tool_name, status)",
102
+ " collector.record_tool_duration(tool_name, duration)",
103
+ " return wrapper",
104
+ "",
105
+ "def instrument_resource(func: Callable, resource_name: str) -> Callable:",
106
+ ' """Wrap a resource function with metrics collection."""',
107
+ " @functools.wraps(func)",
108
+ " async def wrapper(*args, **kwargs) -> Any:",
109
+ " collector = get_metrics_collector()",
110
+ " try:",
111
+ " result = await func(*args, **kwargs)",
112
+ " # Extract URI from args if available for resource_reads metric",
113
+ " if args and len(args) > 0:",
114
+ " uri = str(args[0]) if args[0] else resource_name",
115
+ " else:",
116
+ " uri = resource_name",
117
+ " collector.increment_resource_read(uri)",
118
+ " return result",
119
+ " except Exception as e:",
120
+ " collector.increment_error('resource', type(e).__name__)",
121
+ " raise",
122
+ " return wrapper",
123
+ "",
124
+ "def instrument_prompt(func: Callable, prompt_name: str) -> Callable:",
125
+ ' """Wrap a prompt function with metrics collection."""',
126
+ " @functools.wraps(func)",
127
+ " async def wrapper(*args, **kwargs) -> Any:",
128
+ " collector = get_metrics_collector()",
129
+ " try:",
130
+ " result = await func(*args, **kwargs)",
131
+ " collector.increment_prompt_generation(prompt_name)",
132
+ " return result",
133
+ " except Exception as e:",
134
+ " collector.increment_error('prompt', type(e).__name__)",
135
+ " raise",
136
+ " return wrapper",
137
+ "",
138
+ "# HTTP Request Metrics Middleware",
139
+ "class MetricsMiddleware(BaseHTTPMiddleware):",
140
+ ' """Middleware to collect HTTP request metrics."""',
141
+ "",
142
+ " async def dispatch(self, request: Request, call_next):",
143
+ " collector = get_metrics_collector()",
144
+ " start_time = time.time()",
145
+ " ",
146
+ " # Extract path and method",
147
+ " method = request.method",
148
+ " path = request.url.path",
149
+ " ",
150
+ " try:",
151
+ " response = await call_next(request)",
152
+ " status_code = response.status_code",
153
+ " except Exception as e:",
154
+ " status_code = 500",
155
+ " collector.increment_error('http', type(e).__name__)",
156
+ " raise",
157
+ " finally:",
158
+ " duration = time.time() - start_time",
159
+ " collector.increment_http_request(method, status_code, path)",
160
+ " collector.record_http_duration(method, path, duration)",
161
+ " ",
162
+ " return response",
163
+ "",
164
+ "# Session tracking helpers",
165
+ "import atexit",
166
+ "from contextlib import asynccontextmanager",
167
+ "",
168
+ "# Global server start time for uptime tracking",
169
+ "_server_start_time = time.time()",
170
+ "",
171
+ "def track_session_start():",
172
+ ' """Track when a new session starts."""',
173
+ " collector = get_metrics_collector()",
174
+ " collector.increment_session()",
175
+ "",
176
+ "def track_session_end(start_time: float):",
177
+ ' """Track when a session ends."""',
178
+ " collector = get_metrics_collector()",
179
+ " duration = time.time() - start_time",
180
+ " collector.record_session_duration(duration)",
181
+ "",
182
+ "def update_uptime():",
183
+ ' """Update the uptime metric."""',
184
+ " collector = get_metrics_collector()",
185
+ " uptime = time.time() - _server_start_time",
186
+ " collector.set_uptime(uptime)",
187
+ "",
188
+ "# Initialize uptime tracking",
189
+ "update_uptime()",
190
+ "",
191
+ ]
192
+
193
+
194
+ def generate_session_tracking() -> list[str]:
195
+ """Generate session tracking integration code.
196
+
197
+ Returns:
198
+ List of code lines for session tracking
199
+ """
200
+ return [
201
+ "# Session tracking integration",
202
+ "import asyncio",
203
+ "from typing import Dict",
204
+ "",
205
+ "# Track active sessions",
206
+ "_active_sessions: Dict[str, float] = {}",
207
+ "",
208
+ "# Hook into FastMCP's session lifecycle if available",
209
+ "try:",
210
+ " from fastmcp.server import SessionManager",
211
+ " ",
212
+ " # Monkey patch session creation if possible",
213
+ " _original_create_session = getattr(mcp, '_create_session', None)",
214
+ " if _original_create_session:",
215
+ " async def _patched_create_session(*args, **kwargs):",
216
+ " session_id = str(id(args)) if args else 'unknown'",
217
+ " _active_sessions[session_id] = time.time()",
218
+ " track_session_start()",
219
+ " try:",
220
+ " return await _original_create_session(*args, **kwargs)",
221
+ " except Exception:",
222
+ " # If session creation fails, clean up",
223
+ " if session_id in _active_sessions:",
224
+ " del _active_sessions[session_id]",
225
+ " raise",
226
+ " ",
227
+ " mcp._create_session = _patched_create_session",
228
+ "except (ImportError, AttributeError):",
229
+ " # Fallback: track sessions via request patterns",
230
+ " pass",
231
+ "",
232
+ ]
golf/core/config.py CHANGED
@@ -115,6 +115,12 @@ class Settings(BaseSettings):
115
115
  description="Make Streamable-HTTP transport stateless (new session per request)",
116
116
  )
117
117
 
118
+ # Metrics configuration
119
+ metrics_enabled: bool = Field(
120
+ False, description="Enable Prometheus metrics endpoint"
121
+ )
122
+ metrics_path: str = Field("/metrics", description="Metrics endpoint path")
123
+
118
124
 
119
125
  def find_config_path(start_path: Path | None = None) -> Path | None:
120
126
  """Find the golf config file by searching upwards from the given path.
@@ -0,0 +1,10 @@
1
+ """Golf metrics module for Prometheus-compatible metrics collection."""
2
+
3
+ from golf.metrics.collector import MetricsCollector, get_metrics_collector
4
+ from golf.metrics.registry import init_metrics
5
+
6
+ __all__ = [
7
+ "MetricsCollector",
8
+ "get_metrics_collector",
9
+ "init_metrics",
10
+ ]
@@ -0,0 +1,239 @@
1
+ """Metrics collector for Golf MCP servers."""
2
+
3
+ from typing import Optional
4
+
5
+ # Global metrics collector instance
6
+ _metrics_collector: Optional["MetricsCollector"] = None
7
+
8
+
9
+ class MetricsCollector:
10
+ """Collects metrics for Golf MCP servers using Prometheus client."""
11
+
12
+ def __init__(self, enabled: bool = False) -> None:
13
+ """Initialize the metrics collector.
14
+
15
+ Args:
16
+ enabled: Whether metrics collection is enabled
17
+ """
18
+ self.enabled = enabled
19
+ self._metrics = {}
20
+
21
+ if self.enabled:
22
+ self._init_prometheus_metrics()
23
+
24
+ def _init_prometheus_metrics(self) -> None:
25
+ """Initialize Prometheus metrics if enabled."""
26
+ try:
27
+ from prometheus_client import Counter, Histogram, Gauge
28
+
29
+ # Tool execution metrics
30
+ self._metrics["tool_executions"] = Counter(
31
+ "golf_tool_executions_total",
32
+ "Total number of tool executions",
33
+ ["tool_name", "status"],
34
+ )
35
+
36
+ self._metrics["tool_duration"] = Histogram(
37
+ "golf_tool_duration_seconds",
38
+ "Tool execution duration in seconds",
39
+ ["tool_name"],
40
+ )
41
+
42
+ # HTTP request metrics
43
+ self._metrics["http_requests"] = Counter(
44
+ "golf_http_requests_total",
45
+ "Total number of HTTP requests",
46
+ ["method", "status_code", "path"],
47
+ )
48
+
49
+ self._metrics["http_duration"] = Histogram(
50
+ "golf_http_request_duration_seconds",
51
+ "HTTP request duration in seconds",
52
+ ["method", "path"],
53
+ )
54
+
55
+ # Resource access metrics
56
+ self._metrics["resource_reads"] = Counter(
57
+ "golf_resource_reads_total",
58
+ "Total number of resource reads",
59
+ ["resource_uri"],
60
+ )
61
+
62
+ # Prompt generation metrics
63
+ self._metrics["prompt_generations"] = Counter(
64
+ "golf_prompt_generations_total",
65
+ "Total number of prompt generations",
66
+ ["prompt_name"],
67
+ )
68
+
69
+ # Error metrics
70
+ self._metrics["errors"] = Counter(
71
+ "golf_errors_total",
72
+ "Total number of errors",
73
+ ["component_type", "error_type"],
74
+ )
75
+
76
+ # Session metrics
77
+ self._metrics["sessions_total"] = Counter(
78
+ "golf_sessions_total", "Total number of sessions created"
79
+ )
80
+
81
+ self._metrics["session_duration"] = Histogram(
82
+ "golf_session_duration_seconds", "Session duration in seconds"
83
+ )
84
+
85
+ # System metrics
86
+ self._metrics["uptime"] = Gauge(
87
+ "golf_uptime_seconds", "Server uptime in seconds"
88
+ )
89
+
90
+ except ImportError:
91
+ # Prometheus client not available, disable metrics
92
+ self.enabled = False
93
+
94
+ def increment_tool_execution(self, tool_name: str, status: str) -> None:
95
+ """Record a tool execution.
96
+
97
+ Args:
98
+ tool_name: Name of the tool that was executed
99
+ status: Execution status ('success' or 'error')
100
+ """
101
+ if not self.enabled or "tool_executions" not in self._metrics:
102
+ return
103
+
104
+ self._metrics["tool_executions"].labels(
105
+ tool_name=tool_name, status=status
106
+ ).inc()
107
+
108
+ def record_tool_duration(self, tool_name: str, duration: float) -> None:
109
+ """Record tool execution duration.
110
+
111
+ Args:
112
+ tool_name: Name of the tool
113
+ duration: Execution duration in seconds
114
+ """
115
+ if not self.enabled or "tool_duration" not in self._metrics:
116
+ return
117
+
118
+ self._metrics["tool_duration"].labels(tool_name=tool_name).observe(duration)
119
+
120
+ def increment_http_request(self, method: str, status_code: int, path: str) -> None:
121
+ """Record an HTTP request.
122
+
123
+ Args:
124
+ method: HTTP method (GET, POST, etc.)
125
+ status_code: HTTP status code
126
+ path: Request path
127
+ """
128
+ if not self.enabled or "http_requests" not in self._metrics:
129
+ return
130
+
131
+ self._metrics["http_requests"].labels(
132
+ method=method, status_code=str(status_code), path=path
133
+ ).inc()
134
+
135
+ def record_http_duration(self, method: str, path: str, duration: float) -> None:
136
+ """Record HTTP request duration.
137
+
138
+ Args:
139
+ method: HTTP method
140
+ path: Request path
141
+ duration: Request duration in seconds
142
+ """
143
+ if not self.enabled or "http_duration" not in self._metrics:
144
+ return
145
+
146
+ self._metrics["http_duration"].labels(method=method, path=path).observe(
147
+ duration
148
+ )
149
+
150
+ def increment_resource_read(self, resource_uri: str) -> None:
151
+ """Record a resource read.
152
+
153
+ Args:
154
+ resource_uri: URI of the resource that was read
155
+ """
156
+ if not self.enabled or "resource_reads" not in self._metrics:
157
+ return
158
+
159
+ self._metrics["resource_reads"].labels(resource_uri=resource_uri).inc()
160
+
161
+ def increment_prompt_generation(self, prompt_name: str) -> None:
162
+ """Record a prompt generation.
163
+
164
+ Args:
165
+ prompt_name: Name of the prompt that was generated
166
+ """
167
+ if not self.enabled or "prompt_generations" not in self._metrics:
168
+ return
169
+
170
+ self._metrics["prompt_generations"].labels(prompt_name=prompt_name).inc()
171
+
172
+ def increment_error(self, component_type: str, error_type: str) -> None:
173
+ """Record an error.
174
+
175
+ Args:
176
+ component_type: Type of component ('tool', 'resource', 'prompt', 'http')
177
+ error_type: Type of error ('timeout', 'auth_error', 'validation_error', etc.)
178
+ """
179
+ if not self.enabled or "errors" not in self._metrics:
180
+ return
181
+
182
+ self._metrics["errors"].labels(
183
+ component_type=component_type, error_type=error_type
184
+ ).inc()
185
+
186
+ def increment_session(self) -> None:
187
+ """Record a new session."""
188
+ if not self.enabled or "sessions_total" not in self._metrics:
189
+ return
190
+
191
+ self._metrics["sessions_total"].inc()
192
+
193
+ def record_session_duration(self, duration: float) -> None:
194
+ """Record session duration.
195
+
196
+ Args:
197
+ duration: Session duration in seconds
198
+ """
199
+ if not self.enabled or "session_duration" not in self._metrics:
200
+ return
201
+
202
+ self._metrics["session_duration"].observe(duration)
203
+
204
+ def set_uptime(self, seconds: float) -> None:
205
+ """Set the server uptime.
206
+
207
+ Args:
208
+ seconds: Server uptime in seconds
209
+ """
210
+ if not self.enabled or "uptime" not in self._metrics:
211
+ return
212
+
213
+ self._metrics["uptime"].set(seconds)
214
+
215
+
216
+ def init_metrics_collector(enabled: bool = False) -> MetricsCollector:
217
+ """Initialize the global metrics collector.
218
+
219
+ Args:
220
+ enabled: Whether to enable metrics collection
221
+
222
+ Returns:
223
+ The initialized metrics collector
224
+ """
225
+ global _metrics_collector
226
+ _metrics_collector = MetricsCollector(enabled=enabled)
227
+ return _metrics_collector
228
+
229
+
230
+ def get_metrics_collector() -> MetricsCollector:
231
+ """Get the global metrics collector instance.
232
+
233
+ Returns:
234
+ The metrics collector, or a disabled one if not initialized
235
+ """
236
+ global _metrics_collector
237
+ if _metrics_collector is None:
238
+ _metrics_collector = MetricsCollector(enabled=False)
239
+ return _metrics_collector
@@ -0,0 +1,12 @@
1
+ """Metrics registry for Golf MCP servers."""
2
+
3
+ from golf.metrics.collector import init_metrics_collector
4
+
5
+
6
+ def init_metrics(enabled: bool = False) -> None:
7
+ """Initialize the metrics system.
8
+
9
+ Args:
10
+ enabled: Whether to enable metrics collection
11
+ """
12
+ init_metrics_collector(enabled=enabled)
@@ -4,9 +4,11 @@ import asyncio
4
4
  import functools
5
5
  import os
6
6
  import sys
7
+ import time
7
8
  from collections.abc import Callable
8
9
  from contextlib import asynccontextmanager
9
10
  from typing import TypeVar
11
+ from collections import OrderedDict
10
12
 
11
13
  from opentelemetry import baggage, trace
12
14
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
@@ -15,6 +17,9 @@ from opentelemetry.sdk.trace import TracerProvider
15
17
  from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
16
18
  from opentelemetry.trace import Status, StatusCode
17
19
 
20
+ from starlette.middleware.base import BaseHTTPMiddleware
21
+ from starlette.requests import Request
22
+
18
23
  T = TypeVar("T")
19
24
 
20
25
  # Global tracer instance
@@ -163,6 +168,11 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
163
168
 
164
169
  @functools.wraps(func)
165
170
  async def async_wrapper(*args, **kwargs):
171
+ # Record metrics timing
172
+ import time
173
+
174
+ start_time = time.time()
175
+
166
176
  # Create a more descriptive span name
167
177
  span_name = f"mcp.tool.{tool_name}.execute"
168
178
 
@@ -205,45 +215,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
205
215
  if session_id_from_baggage:
206
216
  span.set_attribute("mcp.session.id", session_id_from_baggage)
207
217
 
208
- # Add tool arguments as span attributes (be careful with sensitive data)
209
- for i, arg in enumerate(args):
210
- if isinstance(arg, str | int | float | bool) or arg is None:
211
- span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
212
- elif hasattr(arg, "__dict__"):
213
- # For objects, just record the type
214
- span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
215
-
216
- # Add named arguments with better naming
217
- for key, value in kwargs.items():
218
- if key != "ctx":
219
- if value is None:
220
- span.set_attribute(f"mcp.tool.input.{key}", "null")
221
- elif isinstance(value, str | int | float | bool):
222
- span.set_attribute(f"mcp.tool.input.{key}", str(value))
223
- elif isinstance(value, list | tuple):
224
- span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
225
- span.set_attribute(f"mcp.tool.input.{key}.type", "array")
226
- elif isinstance(value, dict):
227
- span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
228
- span.set_attribute(f"mcp.tool.input.{key}.type", "object")
229
- # Only show first few keys to avoid exceeding attribute limits
230
- if len(value) > 0 and len(value) <= 5:
231
- keys_list = list(value.keys())[:5]
232
- # Limit key length and join
233
- truncated_keys = [
234
- str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
235
- for k in keys_list
236
- ]
237
- span.set_attribute(
238
- f"mcp.tool.input.{key}.sample_keys",
239
- ",".join(truncated_keys),
240
- )
241
- else:
242
- # For other types, at least record the type
243
- span.set_attribute(
244
- f"mcp.tool.input.{key}.type", type(value).__name__
245
- )
246
-
247
218
  # Add event for tool execution start
248
219
  span.add_event("tool.execution.started", {"tool.name": tool_name})
249
220
 
@@ -254,6 +225,19 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
254
225
  # Add event for successful completion
255
226
  span.add_event("tool.execution.completed", {"tool.name": tool_name})
256
227
 
228
+ # Record metrics for successful execution
229
+ try:
230
+ from golf.metrics import get_metrics_collector
231
+
232
+ metrics_collector = get_metrics_collector()
233
+ metrics_collector.increment_tool_execution(tool_name, "success")
234
+ metrics_collector.record_tool_duration(
235
+ tool_name, time.time() - start_time
236
+ )
237
+ except ImportError:
238
+ # Metrics not available, continue without metrics
239
+ pass
240
+
257
241
  # Capture result metadata with better structure
258
242
  if result is not None:
259
243
  if isinstance(result, str | int | float | bool):
@@ -298,10 +282,27 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
298
282
  "error.message": str(e),
299
283
  },
300
284
  )
285
+
286
+ # Record metrics for failed execution
287
+ try:
288
+ from golf.metrics import get_metrics_collector
289
+
290
+ metrics_collector = get_metrics_collector()
291
+ metrics_collector.increment_tool_execution(tool_name, "error")
292
+ metrics_collector.increment_error("tool", type(e).__name__)
293
+ except ImportError:
294
+ # Metrics not available, continue without metrics
295
+ pass
296
+
301
297
  raise
302
298
 
303
299
  @functools.wraps(func)
304
300
  def sync_wrapper(*args, **kwargs):
301
+ # Record metrics timing
302
+ import time
303
+
304
+ start_time = time.time()
305
+
305
306
  # Create a more descriptive span name
306
307
  span_name = f"mcp.tool.{tool_name}.execute"
307
308
 
@@ -344,45 +345,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
344
345
  if session_id_from_baggage:
345
346
  span.set_attribute("mcp.session.id", session_id_from_baggage)
346
347
 
347
- # Add tool arguments as span attributes (be careful with sensitive data)
348
- for i, arg in enumerate(args):
349
- if isinstance(arg, str | int | float | bool) or arg is None:
350
- span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
351
- elif hasattr(arg, "__dict__"):
352
- # For objects, just record the type
353
- span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
354
-
355
- # Add named arguments with better naming
356
- for key, value in kwargs.items():
357
- if key != "ctx":
358
- if value is None:
359
- span.set_attribute(f"mcp.tool.input.{key}", "null")
360
- elif isinstance(value, str | int | float | bool):
361
- span.set_attribute(f"mcp.tool.input.{key}", str(value))
362
- elif isinstance(value, list | tuple):
363
- span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
364
- span.set_attribute(f"mcp.tool.input.{key}.type", "array")
365
- elif isinstance(value, dict):
366
- span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
367
- span.set_attribute(f"mcp.tool.input.{key}.type", "object")
368
- # Only show first few keys to avoid exceeding attribute limits
369
- if len(value) > 0 and len(value) <= 5:
370
- keys_list = list(value.keys())[:5]
371
- # Limit key length and join
372
- truncated_keys = [
373
- str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
374
- for k in keys_list
375
- ]
376
- span.set_attribute(
377
- f"mcp.tool.input.{key}.sample_keys",
378
- ",".join(truncated_keys),
379
- )
380
- else:
381
- # For other types, at least record the type
382
- span.set_attribute(
383
- f"mcp.tool.input.{key}.type", type(value).__name__
384
- )
385
-
386
348
  # Add event for tool execution start
387
349
  span.add_event("tool.execution.started", {"tool.name": tool_name})
388
350
 
@@ -393,6 +355,19 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
393
355
  # Add event for successful completion
394
356
  span.add_event("tool.execution.completed", {"tool.name": tool_name})
395
357
 
358
+ # Record metrics for successful execution
359
+ try:
360
+ from golf.metrics import get_metrics_collector
361
+
362
+ metrics_collector = get_metrics_collector()
363
+ metrics_collector.increment_tool_execution(tool_name, "success")
364
+ metrics_collector.record_tool_duration(
365
+ tool_name, time.time() - start_time
366
+ )
367
+ except ImportError:
368
+ # Metrics not available, continue without metrics
369
+ pass
370
+
396
371
  # Capture result metadata with better structure
397
372
  if result is not None:
398
373
  if isinstance(result, str | int | float | bool):
@@ -437,6 +412,18 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
437
412
  "error.message": str(e),
438
413
  },
439
414
  )
415
+
416
+ # Record metrics for failed execution
417
+ try:
418
+ from golf.metrics import get_metrics_collector
419
+
420
+ metrics_collector = get_metrics_collector()
421
+ metrics_collector.increment_tool_execution(tool_name, "error")
422
+ metrics_collector.increment_error("tool", type(e).__name__)
423
+ except ImportError:
424
+ # Metrics not available, continue without metrics
425
+ pass
426
+
440
427
  raise
441
428
 
442
429
  # Return appropriate wrapper based on function type
@@ -683,16 +670,6 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
683
670
  if session_id_from_baggage:
684
671
  span.set_attribute("mcp.session.id", session_id_from_baggage)
685
672
 
686
- # Add prompt arguments
687
- for key, value in kwargs.items():
688
- if key != "ctx":
689
- if isinstance(value, str | int | float | bool) or value is None:
690
- span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
691
- else:
692
- span.set_attribute(
693
- f"mcp.prompt.arg.{key}.type", type(value).__name__
694
- )
695
-
696
673
  # Add event for prompt generation start
697
674
  span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
698
675
 
@@ -787,16 +764,6 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
787
764
  if session_id_from_baggage:
788
765
  span.set_attribute("mcp.session.id", session_id_from_baggage)
789
766
 
790
- # Add prompt arguments
791
- for key, value in kwargs.items():
792
- if key != "ctx":
793
- if isinstance(value, str | int | float | bool) or value is None:
794
- span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
795
- else:
796
- span.set_attribute(
797
- f"mcp.prompt.arg.{key}.type", type(value).__name__
798
- )
799
-
800
767
  # Add event for prompt generation start
801
768
  span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
802
769
 
@@ -859,6 +826,238 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
859
826
  return sync_wrapper
860
827
 
861
828
 
829
+ # Add the BoundedSessionTracker class before SessionTracingMiddleware
830
+ class BoundedSessionTracker:
831
+ """Memory-safe session tracker with automatic expiration."""
832
+
833
+ def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600):
834
+ self.max_sessions = max_sessions
835
+ self.session_ttl = session_ttl
836
+ self.sessions: OrderedDict[str, float] = OrderedDict()
837
+ self.last_cleanup = time.time()
838
+
839
+ def track_session(self, session_id: str) -> bool:
840
+ """Track a session, returns True if it's new."""
841
+ current_time = time.time()
842
+
843
+ # Periodic cleanup (every 5 minutes)
844
+ if current_time - self.last_cleanup > 300:
845
+ self._cleanup_expired(current_time)
846
+ self.last_cleanup = current_time
847
+
848
+ # Check if session exists and is still valid
849
+ if session_id in self.sessions:
850
+ # Move to end (mark as recently used)
851
+ self.sessions.move_to_end(session_id)
852
+ return False
853
+
854
+ # New session
855
+ self.sessions[session_id] = current_time
856
+
857
+ # Enforce max size
858
+ while len(self.sessions) > self.max_sessions:
859
+ self.sessions.popitem(last=False) # Remove oldest
860
+
861
+ return True
862
+
863
+ def _cleanup_expired(self, current_time: float):
864
+ """Remove expired sessions."""
865
+ expired = [
866
+ sid
867
+ for sid, timestamp in self.sessions.items()
868
+ if current_time - timestamp > self.session_ttl
869
+ ]
870
+ for sid in expired:
871
+ del self.sessions[sid]
872
+
873
+ def get_active_session_count(self) -> int:
874
+ return len(self.sessions)
875
+
876
+
877
+ class SessionTracingMiddleware(BaseHTTPMiddleware):
878
+ def __init__(self, app):
879
+ super().__init__(app)
880
+ # Use memory-safe session tracker instead of unbounded collections
881
+ self.session_tracker = BoundedSessionTracker(
882
+ max_sessions=1000, session_ttl=3600
883
+ )
884
+
885
+ async def dispatch(self, request: Request, call_next):
886
+ # Record HTTP request timing
887
+ import time
888
+
889
+ start_time = time.time()
890
+
891
+ # Extract session ID from query params or headers
892
+ session_id = request.query_params.get("session_id")
893
+ if not session_id:
894
+ # Check headers as fallback
895
+ session_id = request.headers.get("x-session-id")
896
+
897
+ # Track session metrics using memory-safe tracker
898
+ if session_id:
899
+ is_new_session = self.session_tracker.track_session(session_id)
900
+
901
+ if is_new_session:
902
+ try:
903
+ from golf.metrics import get_metrics_collector
904
+
905
+ metrics_collector = get_metrics_collector()
906
+ metrics_collector.increment_session()
907
+ except ImportError:
908
+ pass
909
+ else:
910
+ # Record session duration for existing sessions
911
+ try:
912
+ from golf.metrics import get_metrics_collector
913
+
914
+ metrics_collector = get_metrics_collector()
915
+ # Use a default duration since we don't track exact start times anymore
916
+ # This is less precise but memory-safe
917
+ metrics_collector.record_session_duration(300.0) # 5 min default
918
+ except ImportError:
919
+ pass
920
+
921
+ # Create a descriptive span name based on the request
922
+ method = request.method
923
+ path = request.url.path
924
+
925
+ # Determine the operation type from the path
926
+ operation_type = "unknown"
927
+ if "/mcp" in path:
928
+ operation_type = "mcp.request"
929
+ elif "/sse" in path:
930
+ operation_type = "sse.stream"
931
+ elif "/auth" in path:
932
+ operation_type = "auth"
933
+
934
+ span_name = f"{operation_type}.{method.lower()}"
935
+
936
+ tracer = get_tracer()
937
+ with tracer.start_as_current_span(span_name) as span:
938
+ # Add comprehensive HTTP attributes
939
+ 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
+ span.set_attribute("http.target", path)
944
+ span.set_attribute(
945
+ "http.user_agent", request.headers.get("user-agent", "unknown")
946
+ )
947
+
948
+ # Add session tracking
949
+ if session_id:
950
+ span.set_attribute("mcp.session.id", session_id)
951
+ span.set_attribute(
952
+ "mcp.session.active_count",
953
+ self.session_tracker.get_active_session_count(),
954
+ )
955
+ # Add to baggage for propagation
956
+ ctx = baggage.set_baggage("mcp.session.id", session_id)
957
+ from opentelemetry import context
958
+
959
+ token = context.attach(ctx)
960
+ else:
961
+ token = None
962
+
963
+ # Add request size if available
964
+ content_length = request.headers.get("content-length")
965
+ if content_length:
966
+ span.set_attribute("http.request.size", int(content_length))
967
+
968
+ # Add event for request start
969
+ span.add_event("http.request.started", {"method": method, "path": path})
970
+
971
+ try:
972
+ response = await call_next(request)
973
+
974
+ # Add response attributes
975
+ 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
+
980
+ # Set span status based on HTTP status
981
+ if response.status_code >= 400:
982
+ span.set_status(
983
+ Status(StatusCode.ERROR, f"HTTP {response.status_code}")
984
+ )
985
+ else:
986
+ span.set_status(Status(StatusCode.OK))
987
+
988
+ # Add event for request completion
989
+ span.add_event(
990
+ "http.request.completed",
991
+ {
992
+ "method": method,
993
+ "path": path,
994
+ "status_code": response.status_code,
995
+ },
996
+ )
997
+
998
+ # Record HTTP request metrics
999
+ try:
1000
+ from golf.metrics import get_metrics_collector
1001
+
1002
+ metrics_collector = get_metrics_collector()
1003
+
1004
+ # Clean up path for metrics (remove query params, normalize)
1005
+ clean_path = path.split("?")[0] # Remove query parameters
1006
+ 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
+ )
1017
+ except ImportError:
1018
+ # Metrics not available, continue without metrics
1019
+ pass
1020
+
1021
+ return response
1022
+ except Exception as e:
1023
+ span.record_exception(e)
1024
+ span.set_status(Status(StatusCode.ERROR, str(e)))
1025
+
1026
+ # Add event for error
1027
+ span.add_event(
1028
+ "http.request.error",
1029
+ {
1030
+ "method": method,
1031
+ "path": path,
1032
+ "error.type": type(e).__name__,
1033
+ "error.message": str(e),
1034
+ },
1035
+ )
1036
+
1037
+ # Record HTTP error metrics
1038
+ try:
1039
+ from golf.metrics import get_metrics_collector
1040
+
1041
+ metrics_collector = get_metrics_collector()
1042
+
1043
+ # Clean up path for metrics
1044
+ clean_path = path.split("?")[0]
1045
+ if clean_path.startswith("/"):
1046
+ clean_path = clean_path[1:] or "root"
1047
+
1048
+ metrics_collector.increment_http_request(
1049
+ method, 500, clean_path
1050
+ ) # Assume 500 for exceptions
1051
+ metrics_collector.increment_error("http", type(e).__name__)
1052
+ except ImportError:
1053
+ pass
1054
+
1055
+ raise
1056
+ finally:
1057
+ if token:
1058
+ context.detach(token)
1059
+
1060
+
862
1061
  @asynccontextmanager
863
1062
  async def telemetry_lifespan(mcp_instance):
864
1063
  """Simplified lifespan for telemetry initialization and cleanup."""
@@ -875,112 +1074,6 @@ async def telemetry_lifespan(mcp_instance):
875
1074
 
876
1075
  # Try to add session tracking middleware if possible
877
1076
  try:
878
- from starlette.middleware.base import BaseHTTPMiddleware
879
- from starlette.requests import Request
880
-
881
- class SessionTracingMiddleware(BaseHTTPMiddleware):
882
- async def dispatch(self, request: Request, call_next):
883
- # Extract session ID from query params or headers
884
- session_id = request.query_params.get("session_id")
885
- if not session_id:
886
- # Check headers as fallback
887
- session_id = request.headers.get("x-session-id")
888
-
889
- # Create a descriptive span name based on the request
890
- method = request.method
891
- path = request.url.path
892
-
893
- # Determine the operation type from the path
894
- operation_type = "unknown"
895
- if "/mcp" in path:
896
- operation_type = "mcp.request"
897
- elif "/sse" in path:
898
- operation_type = "sse.stream"
899
- elif "/auth" in path:
900
- operation_type = "auth"
901
-
902
- span_name = f"{operation_type}.{method.lower()}"
903
-
904
- tracer = get_tracer()
905
- with tracer.start_as_current_span(span_name) as span:
906
- # Add comprehensive HTTP attributes
907
- span.set_attribute("http.method", method)
908
- span.set_attribute("http.url", str(request.url))
909
- span.set_attribute("http.scheme", request.url.scheme)
910
- span.set_attribute("http.host", request.url.hostname or "unknown")
911
- span.set_attribute("http.target", path)
912
- span.set_attribute(
913
- "http.user_agent", request.headers.get("user-agent", "unknown")
914
- )
915
-
916
- # Add session tracking
917
- if session_id:
918
- span.set_attribute("mcp.session.id", session_id)
919
- # Add to baggage for propagation
920
- ctx = baggage.set_baggage("mcp.session.id", session_id)
921
- from opentelemetry import context
922
-
923
- token = context.attach(ctx)
924
- else:
925
- token = None
926
-
927
- # Add request size if available
928
- content_length = request.headers.get("content-length")
929
- if content_length:
930
- span.set_attribute("http.request.size", int(content_length))
931
-
932
- # Add event for request start
933
- span.add_event(
934
- "http.request.started", {"method": method, "path": path}
935
- )
936
-
937
- try:
938
- response = await call_next(request)
939
-
940
- # Add response attributes
941
- span.set_attribute("http.status_code", response.status_code)
942
- span.set_attribute(
943
- "http.status_class", f"{response.status_code // 100}xx"
944
- )
945
-
946
- # Set span status based on HTTP status
947
- if response.status_code >= 400:
948
- span.set_status(
949
- Status(StatusCode.ERROR, f"HTTP {response.status_code}")
950
- )
951
- else:
952
- span.set_status(Status(StatusCode.OK))
953
-
954
- # Add event for request completion
955
- span.add_event(
956
- "http.request.completed",
957
- {
958
- "method": method,
959
- "path": path,
960
- "status_code": response.status_code,
961
- },
962
- )
963
-
964
- return response
965
- except Exception as e:
966
- span.record_exception(e)
967
- span.set_status(Status(StatusCode.ERROR, str(e)))
968
-
969
- # Add event for error
970
- span.add_event(
971
- "http.request.error",
972
- {
973
- "method": method,
974
- "path": path,
975
- "error.type": type(e).__name__,
976
- "error.message": str(e),
977
- },
978
- )
979
- raise
980
- finally:
981
- if token:
982
- context.detach(token)
983
-
984
1077
  # Try to add middleware to FastMCP app if it has Starlette app
985
1078
  if hasattr(mcp_instance, "app") or hasattr(mcp_instance, "_app"):
986
1079
  app = getattr(mcp_instance, "app", getattr(mcp_instance, "_app", None))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: golf-mcp
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Summary: Framework for building MCP servers
5
5
  Author-email: Antoni Gmitruk <antoni@golf.dev>
6
6
  License-Expression: Apache-2.0
@@ -34,6 +34,8 @@ Requires-Dist: opentelemetry-sdk>=1.33.1; extra == "telemetry"
34
34
  Requires-Dist: opentelemetry-instrumentation-asgi>=0.40b0; extra == "telemetry"
35
35
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=0.40b0; extra == "telemetry"
36
36
  Requires-Dist: wrapt>=1.17.0; extra == "telemetry"
37
+ Provides-Extra: metrics
38
+ Requires-Dist: prometheus-client>=0.22.1; extra == "metrics"
37
39
  Dynamic: license-file
38
40
 
39
41
  <div align="center">
@@ -1,4 +1,4 @@
1
- golf/__init__.py,sha256=BzIjnki8Bz3evNWo6bjGxxpLhy_tN9MRYhtM0MnDiWs,23
1
+ golf/__init__.py,sha256=cAJAbAh288a9AL-3yxwFzEM1L26izSJ6wma5aiml_9Y,23
2
2
  golf/auth/__init__.py,sha256=Rj4yUngJklk6xrDCrxqLTtoDAMzF1HcTvy_l8wREeao,4103
3
3
  golf/auth/api_key.py,sha256=LiIraLiH2v7s3yavidaI6BDlAEfK8XnWF15QmaJn9G4,2378
4
4
  golf/auth/helpers.py,sha256=ZogdcHM7J2PN6cL6F6OLZ3gyoUR3dwAFDxOJQ2DW_bc,6526
@@ -11,10 +11,11 @@ golf/commands/build.py,sha256=jhdxB5EwwCC_8PgqdXLUKuBpnycjh0gft3_7EuTo6ro,2319
11
11
  golf/commands/init.py,sha256=DUAvGqOUapWdF2cgWPscqHRvyOZDiajR0F0Wkn_jm-k,10355
12
12
  golf/commands/run.py,sha256=xsiG5LZw4qVt3cRTTfIoWP4Bf4AxNBBJKx0NNfoua40,2884
13
13
  golf/core/__init__.py,sha256=4bKeskJ2fPaZqkz2xQScSa3phRLLrmrczwSL632jv-o,52
14
- golf/core/builder.py,sha256=9TCtZIUaywxMsC3FIj3VFFgEA7sfrER5WaM4BfYAUS8,55807
15
- golf/core/builder_auth.py,sha256=6oGw1HsHdtAjfbJKoyakBrFp2v6FgrO1hFuLR9thiY4,13821
14
+ golf/core/builder.py,sha256=flsXnlwOTUhXFMpmZwoxWgnK_oLj4zpSyqcKuDnXezw,59526
15
+ golf/core/builder_auth.py,sha256=nGgyMTiRAqaNfh1FSvoFe6oTVq9RgfMf9JoFGAv2_do,14050
16
+ golf/core/builder_metrics.py,sha256=j6Gtgd867o46JbDfSNGNsHt1QtV1XHKUJs1z8r4siQM,8830
16
17
  golf/core/builder_telemetry.py,sha256=jobFgRSspLQLuitL4ytk6asSUdTqYcDxGY3sTjkrZd4,2654
17
- golf/core/config.py,sha256=B_GcPDjf2PuOemZNaFMOEuzqtnCchQaJCQkYAuULUgQ,7184
18
+ golf/core/config.py,sha256=6yPtwzVTJauufEnrfUbxsz69H8jC0Ra427oDaRM0-xE,7397
18
19
  golf/core/parser.py,sha256=BQRus1O9zmzSmyavwLVfN8BpYFkbrWUDrgQ7yrYfAKw,42457
19
20
  golf/core/platform.py,sha256=Z2yEi6ilmQCLC_uAD_oZdVO0WvkL4tsyw7sx0vHhysI,6440
20
21
  golf/core/telemetry.py,sha256=CjZ7urbizaRjyFiVBjfGW8V4PmNCG1_quk3FvbVTcjw,15772
@@ -46,11 +47,14 @@ golf/examples/basic/tools/hello.py,sha256=s7Soiq9Wn7oKIvA6Hid8UKB14iyR7HZppIbIT4
46
47
  golf/examples/basic/tools/payments/charge.py,sha256=PIYdFV90hu35H1veLI8ueuYwebzrr5SCTX-x6lxRmU4,1800
47
48
  golf/examples/basic/tools/payments/common.py,sha256=hfyuQRIjrQfSqGjyY55W6pZSD5jL6O0geCE0DGx0v10,1302
48
49
  golf/examples/basic/tools/payments/refund.py,sha256=Qpl4GWvUw-L06oGQMbBzG8pikfCWfBCFcpkRiDOzmyQ,1607
50
+ golf/metrics/__init__.py,sha256=O91y-hj_E9R06gqV8pDZrzHxOIl-1T415Hj9RvFAp3o,283
51
+ golf/metrics/collector.py,sha256=iyRszP8TAAigyOsUFTGdKN8Xeob36LhUvXW9tntJrbA,7617
52
+ golf/metrics/registry.py,sha256=mXQE4Pwf3PopGYjcUu4eGgPDAe085YWcsvcvWk0ny8Q,310
49
53
  golf/telemetry/__init__.py,sha256=ESGCg5HKwTCIfID1e_K7EE0bOWkSzMidlLtdqQgBd0w,396
50
- golf/telemetry/instrumentation.py,sha256=2B0R918-OLpRdyXMTpHuerfc_4osuSi7Lrh9Y2luXfs,43575
51
- golf_mcp-0.1.17.dist-info/licenses/LICENSE,sha256=5_j2f6fTJmvfmUewzElhkpAaXg2grVoxKouOA8ihV6E,11348
52
- golf_mcp-0.1.17.dist-info/METADATA,sha256=-OBF6l8aFo-QYVpnf2c2Uas-8obFK5KYp0EaRvlr_7E,12871
53
- golf_mcp-0.1.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- golf_mcp-0.1.17.dist-info/entry_points.txt,sha256=5y7rHYM8jGpU-nfwdknCm5XsApLulqsnA37MO6BUTYg,43
55
- golf_mcp-0.1.17.dist-info/top_level.txt,sha256=BQToHcBUufdyhp9ONGMIvPE40jMEtmI20lYaKb4hxOg,5
56
- golf_mcp-0.1.17.dist-info/RECORD,,
54
+ golf/telemetry/instrumentation.py,sha256=8n69nYUzsLCvItyYVe7TbxiyfYjgJHqC4sV7GgwL7QI,44652
55
+ golf_mcp-0.1.19.dist-info/licenses/LICENSE,sha256=5_j2f6fTJmvfmUewzElhkpAaXg2grVoxKouOA8ihV6E,11348
56
+ golf_mcp-0.1.19.dist-info/METADATA,sha256=VWwqQwy6gSijjJcjA2A2euWf53gHpZ3KhFRwK8yrE4U,12956
57
+ golf_mcp-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ golf_mcp-0.1.19.dist-info/entry_points.txt,sha256=5y7rHYM8jGpU-nfwdknCm5XsApLulqsnA37MO6BUTYg,43
59
+ golf_mcp-0.1.19.dist-info/top_level.txt,sha256=BQToHcBUufdyhp9ONGMIvPE40jMEtmI20lYaKb4hxOg,5
60
+ golf_mcp-0.1.19.dist-info/RECORD,,