golf-mcp 0.1.10__py3-none-any.whl → 0.1.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +1 -1
- golf/auth/__init__.py +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +202 -82
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +119 -80
- golf/commands/run.py +14 -13
- golf/core/__init__.py +1 -1
- golf/core/builder.py +478 -353
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +169 -69
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +781 -276
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
- golf_mcp-0.1.12.dist-info/RECORD +55 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
- golf_mcp-0.1.10.dist-info/RECORD +0 -55
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/top_level.txt +0 -0
golf/core/telemetry.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Telemetry module for anonymous usage tracking with PostHog."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import hashlib
|
|
5
|
-
import platform
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional, Dict, Any
|
|
8
4
|
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
9
7
|
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
import posthog
|
|
12
12
|
from rich.console import Console
|
|
@@ -23,11 +23,22 @@ POSTHOG_API_KEY = os.environ.get("GOLF_POSTHOG_API_KEY", DEFAULT_POSTHOG_API_KEY
|
|
|
23
23
|
POSTHOG_HOST = "https://us.i.posthog.com"
|
|
24
24
|
|
|
25
25
|
# Telemetry state
|
|
26
|
-
_telemetry_enabled:
|
|
27
|
-
_anonymous_id:
|
|
26
|
+
_telemetry_enabled: bool | None = None
|
|
27
|
+
_anonymous_id: str | None = None
|
|
28
28
|
_user_identified: bool = False # Track if we've already identified the user
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _is_test_mode() -> bool:
|
|
32
|
+
"""Check if we're in test mode."""
|
|
33
|
+
return os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_posthog_disabled_in_test_mode() -> None:
|
|
37
|
+
"""Ensure PostHog is disabled when in test mode."""
|
|
38
|
+
if _is_test_mode() and not posthog.disabled:
|
|
39
|
+
posthog.disabled = True
|
|
40
|
+
|
|
41
|
+
|
|
31
42
|
def get_telemetry_config_path() -> Path:
|
|
32
43
|
"""Get the path to the telemetry configuration file."""
|
|
33
44
|
return Path.home() / ".golf" / "telemetry.json"
|
|
@@ -37,12 +48,9 @@ def save_telemetry_preference(enabled: bool) -> None:
|
|
|
37
48
|
"""Save telemetry preference to persistent storage."""
|
|
38
49
|
config_path = get_telemetry_config_path()
|
|
39
50
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
|
|
41
|
-
config = {
|
|
42
|
-
|
|
43
|
-
"version": 1
|
|
44
|
-
}
|
|
45
|
-
|
|
51
|
+
|
|
52
|
+
config = {"enabled": enabled, "version": 1}
|
|
53
|
+
|
|
46
54
|
try:
|
|
47
55
|
with open(config_path, "w") as f:
|
|
48
56
|
json.dump(config, f)
|
|
@@ -51,15 +59,15 @@ def save_telemetry_preference(enabled: bool) -> None:
|
|
|
51
59
|
pass
|
|
52
60
|
|
|
53
61
|
|
|
54
|
-
def load_telemetry_preference() ->
|
|
62
|
+
def load_telemetry_preference() -> bool | None:
|
|
55
63
|
"""Load telemetry preference from persistent storage."""
|
|
56
64
|
config_path = get_telemetry_config_path()
|
|
57
|
-
|
|
65
|
+
|
|
58
66
|
if not config_path.exists():
|
|
59
67
|
return None
|
|
60
|
-
|
|
68
|
+
|
|
61
69
|
try:
|
|
62
|
-
with open(config_path
|
|
70
|
+
with open(config_path) as f:
|
|
63
71
|
config = json.load(f)
|
|
64
72
|
return config.get("enabled")
|
|
65
73
|
except Exception:
|
|
@@ -68,19 +76,25 @@ def load_telemetry_preference() -> Optional[bool]:
|
|
|
68
76
|
|
|
69
77
|
def is_telemetry_enabled() -> bool:
|
|
70
78
|
"""Check if telemetry is enabled.
|
|
71
|
-
|
|
79
|
+
|
|
72
80
|
Checks in order:
|
|
73
81
|
1. Cached value
|
|
74
|
-
2.
|
|
75
|
-
3.
|
|
76
|
-
4.
|
|
82
|
+
2. GOLF_TEST_MODE environment variable (always disabled in test mode)
|
|
83
|
+
3. GOLF_TELEMETRY environment variable
|
|
84
|
+
4. Persistent preference file
|
|
85
|
+
5. Default to True (opt-out model)
|
|
77
86
|
"""
|
|
78
87
|
global _telemetry_enabled
|
|
79
|
-
|
|
88
|
+
|
|
80
89
|
if _telemetry_enabled is not None:
|
|
81
90
|
return _telemetry_enabled
|
|
82
|
-
|
|
83
|
-
# Check
|
|
91
|
+
|
|
92
|
+
# Check if we're in test mode (highest priority after cache)
|
|
93
|
+
if _is_test_mode():
|
|
94
|
+
_telemetry_enabled = False
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Check environment variables (second highest priority)
|
|
84
98
|
env_telemetry = os.environ.get("GOLF_TELEMETRY", "").lower()
|
|
85
99
|
if env_telemetry in ("0", "false", "no", "off"):
|
|
86
100
|
_telemetry_enabled = False
|
|
@@ -88,13 +102,13 @@ def is_telemetry_enabled() -> bool:
|
|
|
88
102
|
elif env_telemetry in ("1", "true", "yes", "on"):
|
|
89
103
|
_telemetry_enabled = True
|
|
90
104
|
return True
|
|
91
|
-
|
|
105
|
+
|
|
92
106
|
# Check persistent preference
|
|
93
107
|
saved_preference = load_telemetry_preference()
|
|
94
108
|
if saved_preference is not None:
|
|
95
109
|
_telemetry_enabled = saved_preference
|
|
96
110
|
return saved_preference
|
|
97
|
-
|
|
111
|
+
|
|
98
112
|
# Default to enabled (opt-out model)
|
|
99
113
|
_telemetry_enabled = True
|
|
100
114
|
return True
|
|
@@ -102,58 +116,64 @@ def is_telemetry_enabled() -> bool:
|
|
|
102
116
|
|
|
103
117
|
def set_telemetry_enabled(enabled: bool, persist: bool = True) -> None:
|
|
104
118
|
"""Set telemetry enabled state.
|
|
105
|
-
|
|
119
|
+
|
|
106
120
|
Args:
|
|
107
121
|
enabled: Whether telemetry should be enabled
|
|
108
122
|
persist: Whether to save this preference persistently
|
|
109
123
|
"""
|
|
110
124
|
global _telemetry_enabled
|
|
111
125
|
_telemetry_enabled = enabled
|
|
112
|
-
|
|
126
|
+
|
|
113
127
|
if persist:
|
|
114
128
|
save_telemetry_preference(enabled)
|
|
115
129
|
|
|
116
130
|
|
|
117
131
|
def get_anonymous_id() -> str:
|
|
118
132
|
"""Get or create a persistent anonymous ID for this machine.
|
|
119
|
-
|
|
133
|
+
|
|
120
134
|
The ID is stored in the user's home directory and is unique per installation.
|
|
121
135
|
"""
|
|
122
136
|
global _anonymous_id
|
|
123
|
-
|
|
137
|
+
|
|
124
138
|
if _anonymous_id:
|
|
125
139
|
return _anonymous_id
|
|
126
|
-
|
|
140
|
+
|
|
127
141
|
# Try to load existing ID
|
|
128
142
|
id_file = Path.home() / ".golf" / "telemetry_id"
|
|
129
|
-
|
|
143
|
+
|
|
130
144
|
if id_file.exists():
|
|
131
145
|
try:
|
|
132
146
|
_anonymous_id = id_file.read_text().strip()
|
|
133
147
|
# Check if ID is in the old format (no hyphen between hash and random component)
|
|
134
148
|
# Old format: golf-[8 chars hash][8 chars random]
|
|
135
149
|
# New format: golf-[8 chars hash]-[8 chars random]
|
|
136
|
-
if
|
|
150
|
+
if (
|
|
151
|
+
_anonymous_id
|
|
152
|
+
and _anonymous_id.startswith("golf-")
|
|
153
|
+
and len(_anonymous_id) == 21
|
|
154
|
+
):
|
|
137
155
|
# This is likely the old format, regenerate
|
|
138
156
|
_anonymous_id = None
|
|
139
157
|
elif _anonymous_id:
|
|
140
158
|
return _anonymous_id
|
|
141
159
|
except Exception:
|
|
142
160
|
pass
|
|
143
|
-
|
|
161
|
+
|
|
144
162
|
# Generate new ID with more unique data
|
|
145
163
|
# Use only non-identifying system information
|
|
146
|
-
|
|
164
|
+
|
|
147
165
|
# Combine non-identifying factors for uniqueness
|
|
148
|
-
machine_data =
|
|
166
|
+
machine_data = (
|
|
167
|
+
f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
|
|
168
|
+
)
|
|
149
169
|
machine_hash = hashlib.sha256(machine_data.encode()).hexdigest()[:8]
|
|
150
|
-
|
|
170
|
+
|
|
151
171
|
# Add a random component to ensure uniqueness
|
|
152
|
-
random_component = str(uuid.uuid4()).split(
|
|
153
|
-
|
|
172
|
+
random_component = str(uuid.uuid4()).split("-")[0] # First 8 chars of UUID
|
|
173
|
+
|
|
154
174
|
# Use hyphen separator for clarity and ensure PostHog treats these as different IDs
|
|
155
175
|
_anonymous_id = f"golf-{machine_hash}-{random_component}"
|
|
156
|
-
|
|
176
|
+
|
|
157
177
|
# Try to save for next time
|
|
158
178
|
try:
|
|
159
179
|
id_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -161,56 +181,70 @@ def get_anonymous_id() -> str:
|
|
|
161
181
|
except Exception:
|
|
162
182
|
# Not critical if we can't save
|
|
163
183
|
pass
|
|
164
|
-
|
|
184
|
+
|
|
165
185
|
return _anonymous_id
|
|
166
186
|
|
|
167
187
|
|
|
168
188
|
def initialize_telemetry() -> None:
|
|
169
189
|
"""Initialize PostHog telemetry if enabled."""
|
|
190
|
+
# Ensure PostHog is disabled in test mode
|
|
191
|
+
_ensure_posthog_disabled_in_test_mode()
|
|
192
|
+
|
|
193
|
+
# Don't initialize if PostHog is disabled (test mode)
|
|
194
|
+
if posthog.disabled:
|
|
195
|
+
return
|
|
196
|
+
|
|
170
197
|
if not is_telemetry_enabled():
|
|
171
198
|
return
|
|
172
|
-
|
|
199
|
+
|
|
173
200
|
# Skip initialization if no valid API key (empty or placeholder)
|
|
174
201
|
if not POSTHOG_API_KEY or POSTHOG_API_KEY.startswith("phc_YOUR"):
|
|
175
202
|
return
|
|
176
|
-
|
|
203
|
+
|
|
177
204
|
try:
|
|
178
205
|
posthog.project_api_key = POSTHOG_API_KEY
|
|
179
206
|
posthog.host = POSTHOG_HOST
|
|
180
|
-
|
|
207
|
+
|
|
181
208
|
# Disable PostHog's own logging to avoid noise
|
|
182
209
|
posthog.disabled = False
|
|
183
210
|
posthog.debug = False
|
|
184
|
-
|
|
211
|
+
|
|
185
212
|
except Exception:
|
|
186
213
|
# Telemetry should never break the application
|
|
187
214
|
pass
|
|
188
215
|
|
|
189
216
|
|
|
190
|
-
def track_event(event_name: str, properties:
|
|
217
|
+
def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
|
|
191
218
|
"""Track an anonymous event with minimal data.
|
|
192
|
-
|
|
219
|
+
|
|
193
220
|
Args:
|
|
194
221
|
event_name: Name of the event (e.g., "cli_init", "cli_build")
|
|
195
222
|
properties: Optional properties to include with the event
|
|
196
223
|
"""
|
|
197
224
|
global _user_identified
|
|
198
|
-
|
|
225
|
+
|
|
226
|
+
# Ensure PostHog is disabled in test mode
|
|
227
|
+
_ensure_posthog_disabled_in_test_mode()
|
|
228
|
+
|
|
229
|
+
# Early return if PostHog is disabled (test mode)
|
|
230
|
+
if posthog.disabled:
|
|
231
|
+
return
|
|
232
|
+
|
|
199
233
|
if not is_telemetry_enabled():
|
|
200
234
|
return
|
|
201
|
-
|
|
235
|
+
|
|
202
236
|
# Skip if no valid API key (empty or placeholder)
|
|
203
237
|
if not POSTHOG_API_KEY or POSTHOG_API_KEY.startswith("phc_YOUR"):
|
|
204
238
|
return
|
|
205
|
-
|
|
239
|
+
|
|
206
240
|
try:
|
|
207
241
|
# Initialize if needed
|
|
208
242
|
if posthog.project_api_key != POSTHOG_API_KEY:
|
|
209
243
|
initialize_telemetry()
|
|
210
|
-
|
|
244
|
+
|
|
211
245
|
# Get anonymous ID
|
|
212
246
|
anonymous_id = get_anonymous_id()
|
|
213
|
-
|
|
247
|
+
|
|
214
248
|
# Only identify the user once per session
|
|
215
249
|
if not _user_identified:
|
|
216
250
|
# Set person properties to differentiate installations
|
|
@@ -222,15 +256,12 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
|
|
|
222
256
|
"python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
|
|
223
257
|
}
|
|
224
258
|
}
|
|
225
|
-
|
|
259
|
+
|
|
226
260
|
# Identify the user with properties
|
|
227
|
-
posthog.identify(
|
|
228
|
-
|
|
229
|
-
properties=person_properties
|
|
230
|
-
)
|
|
231
|
-
|
|
261
|
+
posthog.identify(distinct_id=anonymous_id, properties=person_properties)
|
|
262
|
+
|
|
232
263
|
_user_identified = True
|
|
233
|
-
|
|
264
|
+
|
|
234
265
|
# Only include minimal, non-identifying properties
|
|
235
266
|
safe_properties = {
|
|
236
267
|
"golf_version": __version__,
|
|
@@ -239,43 +270,112 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
|
|
|
239
270
|
# Explicitly disable IP tracking
|
|
240
271
|
"$ip": None,
|
|
241
272
|
}
|
|
242
|
-
|
|
273
|
+
|
|
243
274
|
# Filter properties to only include safe ones
|
|
244
275
|
if properties:
|
|
245
276
|
# Only include specific safe properties
|
|
246
|
-
safe_keys = {
|
|
277
|
+
safe_keys = {
|
|
278
|
+
"success",
|
|
279
|
+
"environment",
|
|
280
|
+
"template",
|
|
281
|
+
"command_type",
|
|
282
|
+
"error_type",
|
|
283
|
+
"error_message",
|
|
284
|
+
}
|
|
247
285
|
for key in safe_keys:
|
|
248
286
|
if key in properties:
|
|
249
287
|
safe_properties[key] = properties[key]
|
|
250
|
-
|
|
288
|
+
|
|
251
289
|
# Send event
|
|
252
290
|
posthog.capture(
|
|
253
291
|
distinct_id=anonymous_id,
|
|
254
292
|
event=event_name,
|
|
255
293
|
properties=safe_properties,
|
|
256
294
|
)
|
|
257
|
-
|
|
295
|
+
|
|
258
296
|
except Exception:
|
|
259
297
|
# Telemetry should never break the application
|
|
260
298
|
pass
|
|
261
299
|
|
|
262
300
|
|
|
263
|
-
def track_command(
|
|
301
|
+
def track_command(
|
|
302
|
+
command: str,
|
|
303
|
+
success: bool = True,
|
|
304
|
+
error_type: str | None = None,
|
|
305
|
+
error_message: str | None = None,
|
|
306
|
+
) -> None:
|
|
264
307
|
"""Track a CLI command execution with minimal info.
|
|
265
|
-
|
|
308
|
+
|
|
266
309
|
Args:
|
|
267
310
|
command: The command being executed (e.g., "init", "build", "run")
|
|
268
311
|
success: Whether the command was successful
|
|
312
|
+
error_type: Type of error if command failed (e.g., "ValueError", "FileNotFoundError")
|
|
313
|
+
error_message: Sanitized error message (no sensitive data)
|
|
314
|
+
"""
|
|
315
|
+
properties = {"success": success}
|
|
316
|
+
|
|
317
|
+
# Add error details if command failed
|
|
318
|
+
if not success and (error_type or error_message):
|
|
319
|
+
if error_type:
|
|
320
|
+
properties["error_type"] = error_type
|
|
321
|
+
if error_message:
|
|
322
|
+
# Sanitize error message - remove file paths and sensitive info
|
|
323
|
+
sanitized_message = _sanitize_error_message(error_message)
|
|
324
|
+
properties["error_message"] = sanitized_message
|
|
325
|
+
|
|
326
|
+
track_event(f"cli_{command}", properties)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _sanitize_error_message(message: str) -> str:
|
|
330
|
+
"""Sanitize error message to remove sensitive information.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
message: Raw error message
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Sanitized error message
|
|
269
337
|
"""
|
|
270
|
-
|
|
271
|
-
|
|
338
|
+
import re
|
|
339
|
+
|
|
340
|
+
# Remove absolute file paths but keep the filename
|
|
341
|
+
# Unix-style paths
|
|
342
|
+
message = re.sub(
|
|
343
|
+
r'/(?:Users|home|var|tmp|opt|usr|etc)/[^\s"\']+/([^/\s"\']+)', r"\1", message
|
|
344
|
+
)
|
|
345
|
+
# Windows-style paths
|
|
346
|
+
message = re.sub(r'[A-Za-z]:\\[^\s"\']+\\([^\\s"\']+)', r"\1", message)
|
|
347
|
+
# Generic path pattern (catches remaining paths)
|
|
348
|
+
message = re.sub(r'(?:^|[\s"])(/[^\s"\']+/)+([^/\s"\']+)', r"\2", message)
|
|
349
|
+
|
|
350
|
+
# Remove potential API keys or tokens (common patterns)
|
|
351
|
+
# Generic API keys (20+ alphanumeric with underscores/hyphens)
|
|
352
|
+
message = re.sub(r"\b[a-zA-Z0-9_-]{32,}\b", "[REDACTED]", message)
|
|
353
|
+
# Bearer tokens
|
|
354
|
+
message = re.sub(r"Bearer\s+[a-zA-Z0-9_.-]+", "Bearer [REDACTED]", message)
|
|
355
|
+
|
|
356
|
+
# Remove email addresses
|
|
357
|
+
message = re.sub(
|
|
358
|
+
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Remove IP addresses
|
|
362
|
+
message = re.sub(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", "[IP]", message)
|
|
363
|
+
|
|
364
|
+
# Remove port numbers in URLs
|
|
365
|
+
message = re.sub(r":[0-9]{2,5}(?=/|$|\s)", ":[PORT]", message)
|
|
366
|
+
|
|
367
|
+
# Truncate to reasonable length
|
|
368
|
+
if len(message) > 200:
|
|
369
|
+
message = message[:197] + "..."
|
|
370
|
+
|
|
371
|
+
return message
|
|
272
372
|
|
|
273
373
|
|
|
274
374
|
def flush() -> None:
|
|
275
375
|
"""Flush any pending telemetry events."""
|
|
276
376
|
if not is_telemetry_enabled():
|
|
277
377
|
return
|
|
278
|
-
|
|
378
|
+
|
|
279
379
|
try:
|
|
280
380
|
posthog.flush()
|
|
281
381
|
except Exception:
|
|
@@ -287,9 +387,9 @@ def shutdown() -> None:
|
|
|
287
387
|
"""Shutdown telemetry and flush pending events."""
|
|
288
388
|
if not is_telemetry_enabled():
|
|
289
389
|
return
|
|
290
|
-
|
|
390
|
+
|
|
291
391
|
try:
|
|
292
392
|
posthog.shutdown()
|
|
293
393
|
except Exception:
|
|
294
394
|
# Ignore shutdown errors
|
|
295
|
-
pass
|
|
395
|
+
pass
|
golf/core/transformer.py
CHANGED
|
@@ -6,21 +6,23 @@ into explicit FastMCP component registrations.
|
|
|
6
6
|
|
|
7
7
|
import ast
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
from golf.core.parser import ParsedComponent
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class ImportTransformer(ast.NodeTransformer):
|
|
15
15
|
"""AST transformer for rewriting imports in component files."""
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
original_path: Path,
|
|
20
|
+
target_path: Path,
|
|
21
|
+
import_map: dict[str, str],
|
|
22
|
+
project_root: Path,
|
|
23
|
+
) -> None:
|
|
22
24
|
"""Initialize the import transformer.
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
Args:
|
|
25
27
|
original_path: Path to the original file
|
|
26
28
|
target_path: Path to the target file
|
|
@@ -31,39 +33,35 @@ class ImportTransformer(ast.NodeTransformer):
|
|
|
31
33
|
self.target_path = target_path
|
|
32
34
|
self.import_map = import_map
|
|
33
35
|
self.project_root = project_root
|
|
34
|
-
|
|
36
|
+
|
|
35
37
|
def visit_Import(self, node: ast.Import) -> Any:
|
|
36
38
|
"""Transform import statements."""
|
|
37
39
|
return node
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
def visit_ImportFrom(self, node: ast.ImportFrom) -> Any:
|
|
40
42
|
"""Transform import from statements."""
|
|
41
43
|
if node.module is None:
|
|
42
44
|
return node
|
|
43
|
-
|
|
45
|
+
|
|
44
46
|
# Handle relative imports
|
|
45
47
|
if node.level > 0:
|
|
46
48
|
# Calculate the source module path
|
|
47
49
|
source_dir = self.original_path.parent
|
|
48
50
|
for _ in range(node.level - 1):
|
|
49
51
|
source_dir = source_dir.parent
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
if node.module:
|
|
52
54
|
source_module = source_dir / node.module.replace(".", "/")
|
|
53
55
|
else:
|
|
54
56
|
source_module = source_dir
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
# Check if this is a common module import
|
|
57
59
|
source_str = str(source_module.relative_to(self.project_root))
|
|
58
60
|
if source_str in self.import_map:
|
|
59
61
|
# Replace with absolute import
|
|
60
62
|
new_module = self.import_map[source_str]
|
|
61
|
-
return ast.ImportFrom(
|
|
62
|
-
|
|
63
|
-
names=node.names,
|
|
64
|
-
level=0
|
|
65
|
-
)
|
|
66
|
-
|
|
63
|
+
return ast.ImportFrom(module=new_module, names=node.names, level=0)
|
|
64
|
+
|
|
67
65
|
return node
|
|
68
66
|
|
|
69
67
|
|
|
@@ -71,18 +69,18 @@ def transform_component(
|
|
|
71
69
|
component: ParsedComponent,
|
|
72
70
|
output_file: Path,
|
|
73
71
|
project_path: Path,
|
|
74
|
-
import_map:
|
|
72
|
+
import_map: dict[str, str],
|
|
75
73
|
source_file: Path = None,
|
|
76
74
|
) -> str:
|
|
77
75
|
"""Transform a GolfMCP component into a standalone FastMCP component.
|
|
78
|
-
|
|
76
|
+
|
|
79
77
|
Args:
|
|
80
78
|
component: Parsed component to transform
|
|
81
79
|
output_file: Path to write the transformed component to
|
|
82
80
|
project_path: Path to the project root
|
|
83
81
|
import_map: Mapping of original module paths to generated paths
|
|
84
82
|
source_file: Optional path to source file (for common.py files)
|
|
85
|
-
|
|
83
|
+
|
|
86
84
|
Returns:
|
|
87
85
|
Generated component code
|
|
88
86
|
"""
|
|
@@ -93,43 +91,41 @@ def transform_component(
|
|
|
93
91
|
file_path = Path(component.file_path)
|
|
94
92
|
else:
|
|
95
93
|
raise ValueError("Either component or source_file must be provided")
|
|
96
|
-
|
|
97
|
-
with open(file_path
|
|
94
|
+
|
|
95
|
+
with open(file_path) as f:
|
|
98
96
|
source_code = f.read()
|
|
99
|
-
|
|
97
|
+
|
|
100
98
|
# Parse the source code into an AST
|
|
101
99
|
tree = ast.parse(source_code)
|
|
102
|
-
|
|
100
|
+
|
|
103
101
|
# Transform imports
|
|
104
|
-
transformer = ImportTransformer(
|
|
105
|
-
file_path,
|
|
106
|
-
output_file,
|
|
107
|
-
import_map,
|
|
108
|
-
project_path
|
|
109
|
-
)
|
|
102
|
+
transformer = ImportTransformer(file_path, output_file, import_map, project_path)
|
|
110
103
|
tree = transformer.visit(tree)
|
|
111
|
-
|
|
104
|
+
|
|
112
105
|
# Get all imports and docstring
|
|
113
106
|
imports = []
|
|
114
107
|
docstring = None
|
|
115
|
-
|
|
108
|
+
|
|
116
109
|
# Find the module docstring if present
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
isinstance(tree.body[0]
|
|
110
|
+
if (
|
|
111
|
+
len(tree.body) > 0
|
|
112
|
+
and isinstance(tree.body[0], ast.Expr)
|
|
113
|
+
and isinstance(tree.body[0].value, ast.Constant)
|
|
114
|
+
and isinstance(tree.body[0].value.value, str)
|
|
115
|
+
):
|
|
120
116
|
docstring = tree.body[0].value.value
|
|
121
|
-
|
|
117
|
+
|
|
122
118
|
# Find imports
|
|
123
119
|
for node in tree.body:
|
|
124
|
-
if isinstance(node,
|
|
120
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
125
121
|
imports.append(node)
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
|
|
123
|
+
# Generate the transformed code
|
|
128
124
|
transformed_imports = ast.unparse(ast.Module(body=imports, type_ignores=[]))
|
|
129
|
-
|
|
125
|
+
|
|
130
126
|
# Build full transformed code
|
|
131
127
|
transformed_code = transformed_imports + "\n\n"
|
|
132
|
-
|
|
128
|
+
|
|
133
129
|
# Add docstring if present, using proper triple quotes for multi-line docstrings
|
|
134
130
|
if docstring:
|
|
135
131
|
# Check if docstring contains newlines
|
|
@@ -139,30 +135,32 @@ def transform_component(
|
|
|
139
135
|
else:
|
|
140
136
|
# Use single quotes for single-line docstrings
|
|
141
137
|
transformed_code += f'"{docstring}"\n\n'
|
|
142
|
-
|
|
138
|
+
|
|
143
139
|
# Add the rest of the code except imports and the original docstring
|
|
144
140
|
remaining_nodes = []
|
|
145
141
|
for node in tree.body:
|
|
146
142
|
# Skip imports
|
|
147
|
-
if isinstance(node,
|
|
143
|
+
if isinstance(node, ast.Import | ast.ImportFrom):
|
|
148
144
|
continue
|
|
149
|
-
|
|
145
|
+
|
|
150
146
|
# Skip the original docstring
|
|
151
|
-
if (
|
|
152
|
-
isinstance(node
|
|
153
|
-
isinstance(node.value
|
|
147
|
+
if (
|
|
148
|
+
isinstance(node, ast.Expr)
|
|
149
|
+
and isinstance(node.value, ast.Constant)
|
|
150
|
+
and isinstance(node.value.value, str)
|
|
151
|
+
):
|
|
154
152
|
continue
|
|
155
|
-
|
|
153
|
+
|
|
156
154
|
remaining_nodes.append(node)
|
|
157
|
-
|
|
155
|
+
|
|
158
156
|
remaining_code = ast.unparse(ast.Module(body=remaining_nodes, type_ignores=[]))
|
|
159
157
|
transformed_code += remaining_code
|
|
160
|
-
|
|
158
|
+
|
|
161
159
|
# Ensure the directory exists
|
|
162
160
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
-
|
|
161
|
+
|
|
164
162
|
# Write the transformed code to the output file
|
|
165
163
|
with open(output_file, "w") as f:
|
|
166
164
|
f.write(transformed_code)
|
|
167
|
-
|
|
168
|
-
return transformed_code
|
|
165
|
+
|
|
166
|
+
return transformed_code
|
golf/examples/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -7,5 +7,5 @@ from golf.auth import configure_api_key
|
|
|
7
7
|
configure_api_key(
|
|
8
8
|
header_name="Authorization",
|
|
9
9
|
header_prefix="Bearer ", # Will handle both "Bearer " and "token " prefixes
|
|
10
|
-
required=True # Reject requests without a valid API key
|
|
11
|
-
)
|
|
10
|
+
required=True, # Reject requests without a valid API key
|
|
11
|
+
)
|