golf-mcp 0.1.16__py3-none-any.whl → 0.1.18__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/core/platform.py ADDED
@@ -0,0 +1,180 @@
1
+ """Platform registration for Golf MCP projects."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import httpx
9
+ from rich.console import Console
10
+
11
+ from golf import __version__
12
+ from golf.core.config import Settings
13
+ from golf.core.parser import ComponentType, ParsedComponent
14
+
15
+ console = Console()
16
+
17
+
18
+ async def register_project_with_platform(
19
+ project_path: Path,
20
+ settings: Settings,
21
+ components: dict[ComponentType, list[ParsedComponent]],
22
+ ) -> bool:
23
+ """Register project with Golf platform during prod build.
24
+
25
+ Args:
26
+ project_path: Path to the project root
27
+ settings: Project settings
28
+ components: Parsed components dictionary
29
+
30
+ Returns:
31
+ True if registration succeeded or was skipped, False if failed
32
+ """
33
+ # Check if platform integration is enabled
34
+ api_key = os.getenv("GOLF_API_KEY")
35
+ if not api_key:
36
+ return True # Skip silently if no API key
37
+
38
+ # Require explicit server ID
39
+ server_id = os.getenv("GOLF_SERVER_ID")
40
+ if not server_id:
41
+ console.print(
42
+ "[yellow]Warning: Platform registration skipped - GOLF_SERVER_ID environment variable required[/yellow]"
43
+ )
44
+ return True # Skip registration but don't fail build
45
+
46
+ # Build metadata payload
47
+ metadata = {
48
+ "project_name": settings.name,
49
+ "description": settings.description,
50
+ "server_id": server_id,
51
+ "components": _build_component_list(components, project_path),
52
+ "build_timestamp": datetime.utcnow().isoformat(),
53
+ "golf_version": __version__,
54
+ "component_counts": _get_component_counts(components),
55
+ "server_config": {
56
+ "host": settings.host,
57
+ "port": settings.port,
58
+ "transport": settings.transport,
59
+ "auth_enabled": bool(settings.auth),
60
+ "telemetry_enabled": settings.opentelemetry_enabled,
61
+ "health_check_enabled": settings.health_check_enabled,
62
+ },
63
+ }
64
+
65
+ try:
66
+ async with httpx.AsyncClient(timeout=10.0) as client:
67
+ response = await client.post(
68
+ "http://localhost:8000/api/resources",
69
+ json=metadata,
70
+ headers={
71
+ "X-Golf-Key": api_key,
72
+ "Content-Type": "application/json",
73
+ "User-Agent": f"Golf-MCP/{__version__}",
74
+ },
75
+ )
76
+ response.raise_for_status()
77
+
78
+ console.print("[green]✓[/green] Registered with Golf platform")
79
+ return True
80
+
81
+ except httpx.TimeoutException:
82
+ console.print("[yellow]Warning: Platform registration timed out[/yellow]")
83
+ return False
84
+ except httpx.HTTPStatusError as e:
85
+ if e.response.status_code == 401:
86
+ console.print(
87
+ "[yellow]Warning: Platform registration failed - invalid API key[/yellow]"
88
+ )
89
+ elif e.response.status_code == 403:
90
+ console.print(
91
+ "[yellow]Warning: Platform registration failed - access denied[/yellow]"
92
+ )
93
+ else:
94
+ console.print(
95
+ f"[yellow]Warning: Platform registration failed - HTTP {e.response.status_code}[/yellow]"
96
+ )
97
+ return False
98
+ except Exception as e:
99
+ console.print(f"[yellow]Warning: Platform registration failed: {e}[/yellow]")
100
+ return False # Don't fail the build
101
+
102
+
103
+ def _build_component_list(
104
+ components: dict[ComponentType, list[ParsedComponent]],
105
+ project_path: Path,
106
+ ) -> list[dict[str, Any]]:
107
+ """Convert parsed components to platform format.
108
+
109
+ Args:
110
+ components: Dictionary of parsed components by type
111
+ project_path: Path to the project root
112
+
113
+ Returns:
114
+ List of component metadata dictionaries
115
+ """
116
+ result = []
117
+
118
+ for comp_type, comp_list in components.items():
119
+ for comp in comp_list:
120
+ # Start with basic component data
121
+ component_data = {
122
+ "name": comp.name,
123
+ "type": comp_type.value,
124
+ "description": comp.docstring,
125
+ "entry_function": comp.entry_function,
126
+ "parent_module": comp.parent_module,
127
+ }
128
+
129
+ # Add file path relative to project root if available
130
+ if comp.file_path:
131
+ try:
132
+ file_path = Path(comp.file_path)
133
+ # Use the provided project_path for relative calculation
134
+ rel_path = file_path.relative_to(project_path)
135
+ component_data["file_path"] = str(rel_path)
136
+ except ValueError:
137
+ # If relative_to fails, try to find a common path or use filename
138
+ component_data["file_path"] = Path(comp.file_path).name
139
+
140
+ # Add schema information only if available and not None
141
+ if hasattr(comp, "input_schema") and comp.input_schema:
142
+ component_data["input_schema"] = comp.input_schema
143
+ if hasattr(comp, "output_schema") and comp.output_schema:
144
+ component_data["output_schema"] = comp.output_schema
145
+
146
+ # Add component-specific fields only if they have values
147
+ if comp_type == ComponentType.RESOURCE:
148
+ if hasattr(comp, "uri_template") and comp.uri_template:
149
+ component_data["uri_template"] = comp.uri_template
150
+
151
+ elif comp_type == ComponentType.TOOL:
152
+ if hasattr(comp, "annotations") and comp.annotations:
153
+ component_data["annotations"] = comp.annotations
154
+
155
+ # Add parameters only if they exist and are not empty
156
+ if hasattr(comp, "parameters") and comp.parameters:
157
+ component_data["parameters"] = comp.parameters
158
+
159
+ result.append(component_data)
160
+
161
+ return result
162
+
163
+
164
+ def _get_component_counts(
165
+ components: dict[ComponentType, list[ParsedComponent]],
166
+ ) -> dict[str, int]:
167
+ """Get component counts by type.
168
+
169
+ Args:
170
+ components: Dictionary of parsed components by type
171
+
172
+ Returns:
173
+ Dictionary with counts for each component type
174
+ """
175
+ return {
176
+ "tools": len(components.get(ComponentType.TOOL, [])),
177
+ "resources": len(components.get(ComponentType.RESOURCE, [])),
178
+ "prompts": len(components.get(ComponentType.PROMPT, [])),
179
+ "total": sum(len(comp_list) for comp_list in components.values()),
180
+ }
golf/core/telemetry.py CHANGED
@@ -109,9 +109,9 @@ def is_telemetry_enabled() -> bool:
109
109
  _telemetry_enabled = saved_preference
110
110
  return saved_preference
111
111
 
112
- # Default to enabled (opt-out model)
113
- _telemetry_enabled = True
114
- return True
112
+ # Default to disabled (opt-in model)
113
+ _telemetry_enabled = False
114
+ return False
115
115
 
116
116
 
117
117
  def set_telemetry_enabled(enabled: bool, persist: bool = True) -> None:
@@ -209,13 +209,24 @@ def initialize_telemetry() -> None:
209
209
  posthog.disabled = False
210
210
  posthog.debug = False
211
211
 
212
+ # Disable IP collection and GeoIP enrichment at the SDK level
213
+ posthog.set_global_event_properties(
214
+ {
215
+ "$ip": "0", # Override IP with dummy value to prevent collection
216
+ "$geoip_disable": True, # Disable all GeoIP enrichment
217
+ }
218
+ )
219
+
212
220
  except Exception:
213
221
  # Telemetry should never break the application
214
222
  pass
215
223
 
216
224
 
217
225
  def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
218
- """Track an anonymous event with minimal data.
226
+ """Track an anonymous event with NO IP address or geolocation data.
227
+
228
+ IP collection and GeoIP enrichment are disabled at the SDK level to ensure
229
+ complete privacy protection. No IP addresses or location data ever reach PostHog.
219
230
 
220
231
  Args:
221
232
  event_name: Name of the event (e.g., "cli_init", "cli_build")
@@ -257,8 +268,16 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
257
268
  }
258
269
  }
259
270
 
260
- # Identify the user with properties
261
- posthog.identify(distinct_id=anonymous_id, properties=person_properties)
271
+ # Identify the user with properties (IP tracking disabled)
272
+ posthog.identify(
273
+ distinct_id=anonymous_id,
274
+ properties={
275
+ **person_properties,
276
+ # Explicitly disable IP tracking in identify call
277
+ "$ip": "0",
278
+ "$geoip_disable": True,
279
+ },
280
+ )
262
281
 
263
282
  _user_identified = True
264
283
 
@@ -267,8 +286,9 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
267
286
  "golf_version": __version__,
268
287
  "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
269
288
  "os": platform.system(),
270
- # Explicitly disable IP tracking
271
- "$ip": None,
289
+ # Explicitly disable IP tracking and GeoIP enrichment
290
+ "$ip": "0", # Override IP to prevent collection
291
+ "$geoip_disable": True, # Disable GeoIP enrichment
272
292
  }
273
293
 
274
294
  # Filter properties to only include safe ones
@@ -1,5 +1 @@
1
- GOLF_CLIENT_ID="default-client-id"
2
- GOLF_CLIENT_SECRET="default-secret"
3
- JWT_SECRET="example-jwt-secret-for-development-only"
4
- OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318/v1/traces"
5
- OTEL_SERVICE_NAME="golf-mcp"
1
+ #GOLF_API_KEY=<your-api-key>
@@ -7,26 +7,26 @@ This example demonstrates how to build a GitHub API MCP server using Golf's API
7
7
  This MCP server provides tools for:
8
8
 
9
9
  - **Repository Management**
10
- - `list-repos` - List repositories for users, organizations, or the authenticated user
10
+ - `list_repos` - List repositories for users, organizations, or the authenticated user
11
11
 
12
12
  - **Issue Management**
13
- - `create-issues` - Create new issues with labels
14
- - `list-issues` - List and filter issues by state and labels
13
+ - `create_issues` - Create new issues with labels
14
+ - `list_issues` - List and filter issues by state and labels
15
15
 
16
16
  - **Code Search**
17
- - `code-search` - Search for code across GitHub with language and repository filters
17
+ - `code_search` - Search for code across GitHub with language and repository filters
18
18
 
19
19
  - **User Information**
20
- - `get-users` - Get user profiles or verify authentication
20
+ - `get_users` - Get user profiles or verify authentication
21
21
 
22
22
  ## Tool Naming Convention
23
23
 
24
24
  Golf automatically derives tool names from the file structure:
25
- - `tools/issues/create.py` → `create-issues`
26
- - `tools/issues/list.py` → `list-issues`
27
- - `tools/repos/list.py` → `list-repos`
28
- - `tools/search/code.py` → `code-search`
29
- - `tools/users/get.py` → `get-users`
25
+ - `tools/issues/create.py` → `create_issues`
26
+ - `tools/issues/list.py` → `list_issues`
27
+ - `tools/repos/list.py` → `list_repos`
28
+ - `tools/search/code.py` → `code_search`
29
+ - `tools/users/get.py` → `get_users`
30
30
 
31
31
  ## Configuration
32
32
 
@@ -4,9 +4,5 @@
4
4
  "host": "127.0.0.1",
5
5
  "port": 3000,
6
6
  "transport": "sse",
7
- "health_check_enabled": false,
8
- "health_check_path": "/health",
9
- "health_check_response": "OK",
10
- "opentelemetry_enabled": true,
11
- "opentelemetry_default_exporter": "otlp_http"
7
+ "opentelemetry_enabled": false
12
8
  }
@@ -1,5 +1,4 @@
1
- GOLF_CLIENT_ID="default-client-id"
2
- GOLF_CLIENT_SECRET="default-secret"
1
+ GITHUB_CLIENT_ID="default-client-id"
2
+ GITHUB_CLIENT_SECRET="default-secret"
3
3
  JWT_SECRET="example-jwt-secret-for-development-only"
4
- OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318/v1/traces"
5
- OTEL_SERVICE_NAME="golf-mcp"
4
+ #GOLF_API_KEY=<your-api-key>
@@ -4,9 +4,5 @@
4
4
  "host": "127.0.0.1",
5
5
  "port": 3000,
6
6
  "transport": "sse",
7
- "health_check_enabled": false,
8
- "health_check_path": "/health",
9
- "health_check_response": "OK",
10
- "opentelemetry_enabled": true,
11
- "opentelemetry_default_exporter": "otlp_http"
7
+ "opentelemetry_enabled": false
12
8
  }
@@ -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)