golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +209 -87
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +81 -53
  12. golf/commands/run.py +30 -15
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +493 -362
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +216 -95
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +455 -310
  42. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.13.dist-info/RECORD +55 -0
  44. golf_mcp-0.1.11.dist-info/RECORD +0 -55
  45. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
  46. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
golf/auth/provider.py CHANGED
@@ -4,107 +4,112 @@ This module defines the ProviderConfig class used to configure
4
4
  OAuth authentication for GolfMCP servers.
5
5
  """
6
6
 
7
- from typing import Dict, List, Optional, Any
7
+ from typing import Any
8
+
8
9
  from pydantic import BaseModel, Field, field_validator
9
10
 
10
11
 
11
12
  class ProviderConfig(BaseModel):
12
13
  """Configuration for an OAuth2 provider.
13
-
14
+
14
15
  This class defines the configuration for an OAuth2 provider,
15
16
  including the endpoints, credentials, and other settings needed
16
17
  to authenticate with the provider.
17
18
  """
18
-
19
+
19
20
  # Provider identification
20
21
  provider: str = Field(
21
- ...,
22
- description="Provider type (e.g., 'github', 'google', 'custom')"
22
+ ..., description="Provider type (e.g., 'github', 'google', 'custom')"
23
23
  )
24
-
24
+
25
25
  # OAuth credentials - names of environment variables to read at runtime
26
- client_id_env_var: str = Field(..., description="Name of environment variable for Client ID")
27
- client_secret_env_var: str = Field(..., description="Name of environment variable for Client Secret")
28
-
26
+ client_id_env_var: str = Field(
27
+ ..., description="Name of environment variable for Client ID"
28
+ )
29
+ client_secret_env_var: str = Field(
30
+ ..., description="Name of environment variable for Client Secret"
31
+ )
32
+
29
33
  # These fields will store the actual values read at runtime in dist/server.py
30
34
  # They are made optional here as they are resolved in the generated code.
31
- client_id: Optional[str] = Field(None, description="OAuth client ID (resolved at runtime)")
32
- client_secret: Optional[str] = Field(None, description="OAuth client secret (resolved at runtime)")
35
+ client_id: str | None = Field(
36
+ None, description="OAuth client ID (resolved at runtime)"
37
+ )
38
+ client_secret: str | None = Field(
39
+ None, description="OAuth client secret (resolved at runtime)"
40
+ )
33
41
 
34
42
  # OAuth endpoints (can be baked in)
35
43
  authorize_url: str = Field(..., description="Authorization endpoint URL")
36
44
  token_url: str = Field(..., description="Token endpoint URL")
37
- userinfo_url: Optional[str] = Field(
38
- None,
39
- description="User info endpoint URL (for OIDC providers)"
45
+ userinfo_url: str | None = Field(
46
+ None, description="User info endpoint URL (for OIDC providers)"
40
47
  )
41
-
42
- jwks_uri: Optional[str] = Field(
43
- None,
44
- description="JSON Web Key Set URI (for token validation)"
48
+
49
+ jwks_uri: str | None = Field(
50
+ None, description="JSON Web Key Set URI (for token validation)"
45
51
  )
46
-
47
- scopes: List[str] = Field(
48
- default_factory=list,
49
- description="OAuth scopes to request from the provider"
52
+
53
+ scopes: list[str] = Field(
54
+ default_factory=list, description="OAuth scopes to request from the provider"
50
55
  )
51
-
52
- issuer_url: Optional[str] = Field(
53
- None,
54
- description="OIDC issuer URL for discovery (if using OIDC) - will be overridden by runtime value in server.py"
56
+
57
+ issuer_url: str | None = Field(
58
+ None,
59
+ description="OIDC issuer URL for discovery (if using OIDC) - will be overridden by runtime value in server.py",
55
60
  )
56
-
61
+
57
62
  callback_path: str = Field(
58
- "/auth/callback",
59
- description="Path on this server where the IdP should redirect after authentication"
63
+ "/auth/callback",
64
+ description="Path on this server where the IdP should redirect after authentication",
60
65
  )
61
-
66
+
62
67
  # JWT configuration
63
- jwt_secret_env_var: str = Field(..., description="Name of environment variable for JWT Secret")
64
- jwt_secret: Optional[str] = Field(None, description="Secret key for signing JWT tokens (resolved at runtime)")
68
+ jwt_secret_env_var: str = Field(
69
+ ..., description="Name of environment variable for JWT Secret"
70
+ )
71
+ jwt_secret: str | None = Field(
72
+ None, description="Secret key for signing JWT tokens (resolved at runtime)"
73
+ )
65
74
  token_expiration: int = Field(
66
- 3600,
67
- description="JWT token expiration time in seconds",
68
- ge=60,
69
- le=86400
75
+ 3600, description="JWT token expiration time in seconds", ge=60, le=86400
70
76
  )
71
-
72
- settings: Dict[str, Any] = Field(
73
- default_factory=dict,
74
- description="Additional provider-specific settings"
77
+
78
+ settings: dict[str, Any] = Field(
79
+ default_factory=dict, description="Additional provider-specific settings"
75
80
  )
76
-
77
- @field_validator('provider')
81
+
82
+ @field_validator("provider")
78
83
  @classmethod
79
84
  def validate_provider(cls, value: str) -> str:
80
85
  """Validate the provider type.
81
-
86
+
82
87
  Ensures the provider type is a valid, supported provider.
83
-
88
+
84
89
  Args:
85
90
  value: The provider type
86
-
91
+
87
92
  Returns:
88
93
  The validated provider type
89
-
94
+
90
95
  Raises:
91
96
  ValueError: If the provider type is not supported
92
97
  """
93
- known_providers = {'custom', 'github', 'google', 'jwks'}
94
-
95
- if value not in known_providers and not value.startswith('custom:'):
98
+ known_providers = {"custom", "github", "google", "jwks"}
99
+
100
+ if value not in known_providers and not value.startswith("custom:"):
96
101
  raise ValueError(
97
102
  f"Unknown provider: '{value}'. Must be one of {known_providers} "
98
103
  "or start with 'custom:'"
99
104
  )
100
105
  return value
101
-
106
+
102
107
  def get_provider_name(self) -> str:
103
108
  """Get a clean provider name for display purposes.
104
-
109
+
105
110
  Returns:
106
111
  A human-readable provider name
107
112
  """
108
- if self.provider.startswith('custom:'):
113
+ if self.provider.startswith("custom:"):
109
114
  return self.provider[7:] # Remove 'custom:' prefix
110
- return self.provider.capitalize()
115
+ return self.provider.capitalize()
golf/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- """CLI package for the GolfMCP framework."""
1
+ """CLI package for the GolfMCP framework."""
golf/cli/main.py CHANGED
@@ -1,17 +1,23 @@
1
1
  """CLI entry points for GolfMCP."""
2
2
 
3
+ import atexit
3
4
  import os
4
5
  from pathlib import Path
5
- from typing import Optional
6
- import atexit
7
6
 
8
7
  import typer
9
8
  from rich.console import Console
10
9
  from rich.panel import Panel
11
10
 
12
11
  from golf import __version__
13
- from golf.core.config import load_settings, find_project_root
14
- from golf.core.telemetry import is_telemetry_enabled, shutdown, set_telemetry_enabled, track_event
12
+ from golf.core.config import find_project_root, load_settings
13
+ from golf.core.telemetry import (
14
+ is_telemetry_enabled,
15
+ set_telemetry_enabled,
16
+ shutdown,
17
+ track_event,
18
+ track_detailed_error,
19
+ _detect_execution_environment,
20
+ )
15
21
 
16
22
  # Create console for rich output
17
23
  console = Console()
@@ -48,24 +54,39 @@ def callback(
48
54
  False, "--verbose", "-v", help="Increase verbosity of output."
49
55
  ),
50
56
  no_telemetry: bool = typer.Option(
51
- False, "--no-telemetry", help="Disable telemetry collection (persists for future commands)."
57
+ False,
58
+ "--no-telemetry",
59
+ help="Disable telemetry collection (persists for future commands).",
60
+ ),
61
+ test: bool = typer.Option(
62
+ False,
63
+ "--test",
64
+ hidden=True,
65
+ help="Run in test mode (disables telemetry for this execution only).",
52
66
  ),
53
67
  ) -> None:
54
68
  """GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate."""
55
69
  # Set verbosity in environment for other components to access
56
70
  if verbose:
57
71
  os.environ["GOLF_VERBOSE"] = "1"
58
-
59
- # Set telemetry preference if flag is used
72
+
73
+ # Set test mode if flag is used (temporary, just for this execution)
74
+ if test:
75
+ set_telemetry_enabled(False, persist=False)
76
+ os.environ["GOLF_TEST_MODE"] = "1"
77
+
78
+ # Set telemetry preference if flag is used (permanent)
60
79
  if no_telemetry:
61
80
  set_telemetry_enabled(False, persist=True)
62
- console.print("[dim]Telemetry has been disabled. You can re-enable it with: golf telemetry enable[/dim]")
81
+ console.print(
82
+ "[dim]Telemetry has been disabled. You can re-enable it with: golf telemetry enable[/dim]"
83
+ )
63
84
 
64
85
 
65
86
  @app.command()
66
87
  def init(
67
88
  project_name: str = typer.Argument(..., help="Name of the project to create"),
68
- output_dir: Optional[Path] = typer.Option(
89
+ output_dir: Path | None = typer.Option(
69
90
  None, "--output-dir", "-o", help="Directory to create the project in"
70
91
  ),
71
92
  template: str = typer.Option(
@@ -73,19 +94,21 @@ def init(
73
94
  ),
74
95
  ) -> None:
75
96
  """Initialize a new GolfMCP project.
76
-
97
+
77
98
  Creates a new directory with the project scaffold, including
78
99
  examples for tools, resources, and prompts.
79
100
  """
80
101
  # Import here to avoid circular imports
81
102
  from golf.commands.init import initialize_project
82
-
103
+
83
104
  # Use the current directory if no output directory is specified
84
105
  if output_dir is None:
85
106
  output_dir = Path.cwd() / project_name
86
-
107
+
87
108
  # Execute the initialization command (it handles its own tracking)
88
- initialize_project(project_name=project_name, output_dir=output_dir, template=template)
109
+ initialize_project(
110
+ project_name=project_name, output_dir=output_dir, template=template
111
+ )
89
112
 
90
113
 
91
114
  # Create a build group with subcommands
@@ -95,179 +118,278 @@ app.add_typer(build_app, name="build")
95
118
 
96
119
  @build_app.command("dev")
97
120
  def build_dev(
98
- output_dir: Optional[str] = typer.Option(
121
+ output_dir: str | None = typer.Option(
99
122
  None, "--output-dir", "-o", help="Directory to output the built project"
100
- )
101
- ):
123
+ ),
124
+ ) -> None:
102
125
  """Build a development version with environment variables copied."""
103
126
  # Find project root directory
104
127
  project_root, config_path = find_project_root()
105
-
128
+
106
129
  if not project_root:
107
- console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
130
+ console.print(
131
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
132
+ )
108
133
  console.print("Run 'golf init <project_name>' to create a new project.")
109
- track_event("cli_build_failed", {"success": False, "environment": "dev", "error_type": "NoProjectFound", "error_message": "No GolfMCP project found"})
134
+ track_event(
135
+ "cli_build_failed",
136
+ {
137
+ "success": False,
138
+ "environment": "dev",
139
+ "error_type": "NoProjectFound",
140
+ "error_message": "No GolfMCP project found",
141
+ },
142
+ )
110
143
  raise typer.Exit(code=1)
111
-
144
+
112
145
  # Load settings from the found project
113
146
  settings = load_settings(project_root)
114
-
147
+
115
148
  # Set default output directory if not specified
116
- if output_dir is None:
117
- output_dir = project_root / "dist"
118
- else:
119
- output_dir = Path(output_dir)
120
-
149
+ output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
150
+
121
151
  try:
122
152
  # Build the project with environment variables copied
123
153
  from golf.commands.build import build_project
124
- build_project(project_root, settings, output_dir, build_env="dev", copy_env=True)
154
+
155
+ build_project(
156
+ project_root, settings, output_dir, build_env="dev", copy_env=True
157
+ )
125
158
  # Track successful build with environment
126
159
  track_event("cli_build_success", {"success": True, "environment": "dev"})
127
160
  except Exception as e:
128
- error_type = type(e).__name__
129
- error_message = str(e)
130
- track_event("cli_build_failed", {"success": False, "environment": "dev", "error_type": error_type, "error_message": error_message})
161
+ track_detailed_error(
162
+ "cli_build_failed",
163
+ e,
164
+ context="Development build with environment variables",
165
+ operation="build_dev",
166
+ additional_props={"environment": "dev", "copy_env": True}
167
+ )
131
168
  raise
132
169
 
133
170
 
134
171
  @build_app.command("prod")
135
172
  def build_prod(
136
- output_dir: Optional[str] = typer.Option(
173
+ output_dir: str | None = typer.Option(
137
174
  None, "--output-dir", "-o", help="Directory to output the built project"
138
- )
139
- ):
175
+ ),
176
+ ) -> None:
140
177
  """Build a production version without copying environment variables."""
141
178
  # Find project root directory
142
179
  project_root, config_path = find_project_root()
143
-
180
+
144
181
  if not project_root:
145
- console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
182
+ console.print(
183
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
184
+ )
146
185
  console.print("Run 'golf init <project_name>' to create a new project.")
147
- track_event("cli_build_failed", {"success": False, "environment": "prod", "error_type": "NoProjectFound", "error_message": "No GolfMCP project found"})
186
+ track_event(
187
+ "cli_build_failed",
188
+ {
189
+ "success": False,
190
+ "environment": "prod",
191
+ "error_type": "NoProjectFound",
192
+ "error_message": "No GolfMCP project found",
193
+ },
194
+ )
148
195
  raise typer.Exit(code=1)
149
-
196
+
150
197
  # Load settings from the found project
151
198
  settings = load_settings(project_root)
152
-
199
+
153
200
  # Set default output directory if not specified
154
- if output_dir is None:
155
- output_dir = project_root / "dist"
156
- else:
157
- output_dir = Path(output_dir)
158
-
201
+ output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
202
+
159
203
  try:
160
204
  # Build the project without copying environment variables
161
205
  from golf.commands.build import build_project
162
- build_project(project_root, settings, output_dir, build_env="prod", copy_env=False)
206
+
207
+ build_project(
208
+ project_root, settings, output_dir, build_env="prod", copy_env=False
209
+ )
163
210
  # Track successful build with environment
164
211
  track_event("cli_build_success", {"success": True, "environment": "prod"})
165
212
  except Exception as e:
166
- error_type = type(e).__name__
167
- error_message = str(e)
168
- track_event("cli_build_failed", {"success": False, "environment": "prod", "error_type": error_type, "error_message": error_message})
213
+ track_detailed_error(
214
+ "cli_build_failed",
215
+ e,
216
+ context="Production build without environment variables",
217
+ operation="build_prod",
218
+ additional_props={"environment": "prod", "copy_env": False}
219
+ )
169
220
  raise
170
221
 
171
222
 
172
223
  @app.command()
173
224
  def run(
174
- dist_dir: Optional[str] = typer.Option(
225
+ dist_dir: str | None = typer.Option(
175
226
  None, "--dist-dir", "-d", help="Directory containing the built server"
176
227
  ),
177
- host: Optional[str] = typer.Option(
228
+ host: str | None = typer.Option(
178
229
  None, "--host", "-h", help="Host to bind to (overrides settings)"
179
230
  ),
180
- port: Optional[int] = typer.Option(
231
+ port: int | None = typer.Option(
181
232
  None, "--port", "-p", help="Port to bind to (overrides settings)"
182
233
  ),
183
234
  build_first: bool = typer.Option(
184
235
  True, "--build/--no-build", help="Build the project before running"
185
236
  ),
186
- ):
237
+ ) -> None:
187
238
  """Run the built FastMCP server.
188
-
239
+
189
240
  This command runs the built server from the dist directory.
190
241
  By default, it will build the project first if needed.
191
242
  """
192
243
  # Find project root directory
193
244
  project_root, config_path = find_project_root()
194
-
245
+
195
246
  if not project_root:
196
- console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
247
+ console.print(
248
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
249
+ )
197
250
  console.print("Run 'golf init <project_name>' to create a new project.")
198
- track_event("cli_run_failed", {"success": False, "error_type": "NoProjectFound", "error_message": "No GolfMCP project found"})
251
+ track_event(
252
+ "cli_run_failed",
253
+ {
254
+ "success": False,
255
+ "error_type": "NoProjectFound",
256
+ "error_message": "No GolfMCP project found",
257
+ },
258
+ )
199
259
  raise typer.Exit(code=1)
200
-
260
+
201
261
  # Load settings from the found project
202
262
  settings = load_settings(project_root)
203
-
263
+
204
264
  # Set default dist directory if not specified
205
- if dist_dir is None:
206
- dist_dir = project_root / "dist"
207
- else:
208
- dist_dir = Path(dist_dir)
209
-
265
+ dist_dir = project_root / "dist" if dist_dir is None else Path(dist_dir)
266
+
210
267
  # Check if dist directory exists
211
268
  if not dist_dir.exists():
212
269
  if build_first:
213
- console.print(f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]")
270
+ console.print(
271
+ f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]"
272
+ )
214
273
  try:
215
274
  # Build the project
216
275
  from golf.commands.build import build_project
276
+
217
277
  build_project(project_root, settings, dist_dir)
218
278
  except Exception as e:
219
- error_type = type(e).__name__
220
- error_message = str(e)
221
- console.print(f"[bold red]Error building project:[/bold red] {error_message}")
222
- track_event("cli_run_failed", {"success": False, "error_type": f"BuildError.{error_type}", "error_message": error_message})
279
+ console.print(
280
+ f"[bold red]Error building project:[/bold red] {str(e)}"
281
+ )
282
+ track_detailed_error(
283
+ "cli_run_failed",
284
+ e,
285
+ context="Auto-build before running server",
286
+ operation="auto_build_before_run",
287
+ additional_props={"auto_build": True}
288
+ )
223
289
  raise
224
290
  else:
225
- console.print(f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]")
226
- console.print("Run 'golf build' first or use --build to build automatically.")
227
- track_event("cli_run_failed", {"success": False, "error_type": "DistNotFound", "error_message": "Dist directory not found"})
291
+ console.print(
292
+ f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]"
293
+ )
294
+ console.print(
295
+ "Run 'golf build' first or use --build to build automatically."
296
+ )
297
+ track_event(
298
+ "cli_run_failed",
299
+ {
300
+ "success": False,
301
+ "error_type": "DistNotFound",
302
+ "error_message": "Dist directory not found",
303
+ },
304
+ )
228
305
  raise typer.Exit(code=1)
229
-
306
+
230
307
  try:
231
308
  # Import and run the server
232
309
  from golf.commands.run import run_server
310
+
233
311
  return_code = run_server(
234
312
  project_path=project_root,
235
313
  settings=settings,
236
314
  dist_dir=dist_dir,
237
315
  host=host,
238
- port=port
316
+ port=port,
239
317
  )
240
-
241
- # Track based on return code
318
+
319
+ # Track based on return code with better categorization
242
320
  if return_code == 0:
243
321
  track_event("cli_run_success", {"success": True})
322
+ elif return_code in [130, 143, 137, 2]:
323
+ # Intentional shutdowns (not errors):
324
+ # 130: Ctrl+C (SIGINT)
325
+ # 143: SIGTERM (graceful shutdown, e.g., Kubernetes, Docker)
326
+ # 137: SIGKILL (forced shutdown)
327
+ # 2: General interrupt/graceful shutdown
328
+ shutdown_type = {
329
+ 130: "UserInterrupt",
330
+ 143: "GracefulShutdown",
331
+ 137: "ForcedShutdown",
332
+ 2: "Interrupt"
333
+ }.get(return_code, "GracefulShutdown")
334
+
335
+ track_event(
336
+ "cli_run_shutdown",
337
+ {
338
+ "success": True, # Not an error
339
+ "shutdown_type": shutdown_type,
340
+ "exit_code": return_code,
341
+ },
342
+ )
244
343
  else:
245
- track_event("cli_run_failed", {"success": False, "error_type": "NonZeroExit", "error_message": f"Server exited with code {return_code}"})
246
-
344
+ # Actual errors (unexpected exit codes)
345
+ track_event(
346
+ "cli_run_failed",
347
+ {
348
+ "success": False,
349
+ "error_type": "UnexpectedExit",
350
+ "error_message": f"Server process exited unexpectedly with code {return_code}",
351
+ "exit_code": return_code,
352
+ "operation": "server_process_execution",
353
+ "context": "Server process terminated with unexpected exit code",
354
+ # Add execution environment context
355
+ "execution_env": _detect_execution_environment(),
356
+ },
357
+ )
358
+
247
359
  # Exit with the same code as the server
248
360
  if return_code != 0:
249
361
  raise typer.Exit(code=return_code)
250
362
  except Exception as e:
251
- error_type = type(e).__name__
252
- error_message = str(e)
253
- track_event("cli_run_failed", {"success": False, "error_type": error_type, "error_message": error_message})
363
+ track_detailed_error(
364
+ "cli_run_failed",
365
+ e,
366
+ context="Server execution or startup failure",
367
+ operation="run_server_execution",
368
+ additional_props={"has_dist_dir": dist_dir.exists() if dist_dir else False}
369
+ )
254
370
  raise
255
371
 
256
372
 
257
373
  # Add telemetry command group
258
374
  @app.command()
259
375
  def telemetry(
260
- action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'")
261
- ):
376
+ action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'"),
377
+ ) -> None:
262
378
  """Manage telemetry settings."""
263
379
  if action.lower() == "enable":
264
380
  set_telemetry_enabled(True, persist=True)
265
- console.print("[green]✓[/green] Telemetry enabled. Thank you for helping improve Golf!")
381
+ console.print(
382
+ "[green]✓[/green] Telemetry enabled. Thank you for helping improve Golf!"
383
+ )
266
384
  elif action.lower() == "disable":
267
385
  set_telemetry_enabled(False, persist=True)
268
- console.print("[yellow]Telemetry disabled.[/yellow] You can re-enable it anytime with: golf telemetry enable")
386
+ console.print(
387
+ "[yellow]Telemetry disabled.[/yellow] You can re-enable it anytime with: golf telemetry enable"
388
+ )
269
389
  else:
270
- console.print(f"[red]Unknown action '{action}'. Use 'enable' or 'disable'.[/red]")
390
+ console.print(
391
+ f"[red]Unknown action '{action}'. Use 'enable' or 'disable'.[/red]"
392
+ )
271
393
  raise typer.Exit(code=1)
272
394
 
273
395
 
@@ -280,13 +402,13 @@ if __name__ == "__main__":
280
402
  border_style="green",
281
403
  )
282
404
  )
283
-
405
+
284
406
  # Add telemetry notice if enabled
285
407
  if is_telemetry_enabled():
286
408
  console.print(
287
409
  "[dim]📊 Anonymous usage data is collected to improve Golf. "
288
410
  "Disable with: golf telemetry disable[/dim]\n"
289
411
  )
290
-
412
+
291
413
  # Run the CLI app
292
- app()
414
+ app()
golf/commands/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """GolfMCP command implementations."""
2
2
 
3
- from golf.commands import init, build, run
3
+ from golf.commands import build, init, run