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.

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 +202 -82
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +119 -80
  12. golf/commands/run.py +14 -13
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +478 -353
  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 +169 -69
  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 +781 -276
  42. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.12.dist-info/RECORD +55 -0
  44. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
  45. golf_mcp-0.1.10.dist-info/RECORD +0 -55
  46. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.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,21 @@
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
+ )
15
19
 
16
20
  # Create console for rich output
17
21
  console = Console()
@@ -48,24 +52,39 @@ def callback(
48
52
  False, "--verbose", "-v", help="Increase verbosity of output."
49
53
  ),
50
54
  no_telemetry: bool = typer.Option(
51
- False, "--no-telemetry", help="Disable telemetry collection (persists for future commands)."
55
+ False,
56
+ "--no-telemetry",
57
+ help="Disable telemetry collection (persists for future commands).",
58
+ ),
59
+ test: bool = typer.Option(
60
+ False,
61
+ "--test",
62
+ hidden=True,
63
+ help="Run in test mode (disables telemetry for this execution only).",
52
64
  ),
53
65
  ) -> None:
54
66
  """GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate."""
55
67
  # Set verbosity in environment for other components to access
56
68
  if verbose:
57
69
  os.environ["GOLF_VERBOSE"] = "1"
58
-
59
- # Set telemetry preference if flag is used
70
+
71
+ # Set test mode if flag is used (temporary, just for this execution)
72
+ if test:
73
+ set_telemetry_enabled(False, persist=False)
74
+ os.environ["GOLF_TEST_MODE"] = "1"
75
+
76
+ # Set telemetry preference if flag is used (permanent)
60
77
  if no_telemetry:
61
78
  set_telemetry_enabled(False, persist=True)
62
- console.print("[dim]Telemetry has been disabled. You can re-enable it with: golf telemetry enable[/dim]")
79
+ console.print(
80
+ "[dim]Telemetry has been disabled. You can re-enable it with: golf telemetry enable[/dim]"
81
+ )
63
82
 
64
83
 
65
84
  @app.command()
66
85
  def init(
67
86
  project_name: str = typer.Argument(..., help="Name of the project to create"),
68
- output_dir: Optional[Path] = typer.Option(
87
+ output_dir: Path | None = typer.Option(
69
88
  None, "--output-dir", "-o", help="Directory to create the project in"
70
89
  ),
71
90
  template: str = typer.Option(
@@ -73,19 +92,21 @@ def init(
73
92
  ),
74
93
  ) -> None:
75
94
  """Initialize a new GolfMCP project.
76
-
95
+
77
96
  Creates a new directory with the project scaffold, including
78
97
  examples for tools, resources, and prompts.
79
98
  """
80
99
  # Import here to avoid circular imports
81
100
  from golf.commands.init import initialize_project
82
-
101
+
83
102
  # Use the current directory if no output directory is specified
84
103
  if output_dir is None:
85
104
  output_dir = Path.cwd() / project_name
86
-
105
+
87
106
  # Execute the initialization command (it handles its own tracking)
88
- initialize_project(project_name=project_name, output_dir=output_dir, template=template)
107
+ initialize_project(
108
+ project_name=project_name, output_dir=output_dir, template=template
109
+ )
89
110
 
90
111
 
91
112
  # Create a build group with subcommands
@@ -95,166 +116,265 @@ app.add_typer(build_app, name="build")
95
116
 
96
117
  @build_app.command("dev")
97
118
  def build_dev(
98
- output_dir: Optional[str] = typer.Option(
119
+ output_dir: str | None = typer.Option(
99
120
  None, "--output-dir", "-o", help="Directory to output the built project"
100
- )
101
- ):
121
+ ),
122
+ ) -> None:
102
123
  """Build a development version with environment variables copied."""
103
124
  # Find project root directory
104
125
  project_root, config_path = find_project_root()
105
-
126
+
106
127
  if not project_root:
107
- console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
128
+ console.print(
129
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
130
+ )
108
131
  console.print("Run 'golf init <project_name>' to create a new project.")
109
- track_event("cli_build_failed", {"success": False, "environment": "dev"})
132
+ track_event(
133
+ "cli_build_failed",
134
+ {
135
+ "success": False,
136
+ "environment": "dev",
137
+ "error_type": "NoProjectFound",
138
+ "error_message": "No GolfMCP project found",
139
+ },
140
+ )
110
141
  raise typer.Exit(code=1)
111
-
142
+
112
143
  # Load settings from the found project
113
144
  settings = load_settings(project_root)
114
-
145
+
115
146
  # 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
-
147
+ output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
148
+
121
149
  try:
122
150
  # Build the project with environment variables copied
123
151
  from golf.commands.build import build_project
124
- build_project(project_root, settings, output_dir, build_env="dev", copy_env=True)
152
+
153
+ build_project(
154
+ project_root, settings, output_dir, build_env="dev", copy_env=True
155
+ )
125
156
  # Track successful build with environment
126
157
  track_event("cli_build_success", {"success": True, "environment": "dev"})
127
- except Exception:
128
- track_event("cli_build_failed", {"success": False, "environment": "dev"})
158
+ except Exception as e:
159
+ error_type = type(e).__name__
160
+ error_message = str(e)
161
+ track_event(
162
+ "cli_build_failed",
163
+ {
164
+ "success": False,
165
+ "environment": "dev",
166
+ "error_type": error_type,
167
+ "error_message": error_message,
168
+ },
169
+ )
129
170
  raise
130
171
 
131
172
 
132
173
  @build_app.command("prod")
133
174
  def build_prod(
134
- output_dir: Optional[str] = typer.Option(
175
+ output_dir: str | None = typer.Option(
135
176
  None, "--output-dir", "-o", help="Directory to output the built project"
136
- )
137
- ):
177
+ ),
178
+ ) -> None:
138
179
  """Build a production version without copying environment variables."""
139
180
  # Find project root directory
140
181
  project_root, config_path = find_project_root()
141
-
182
+
142
183
  if not project_root:
143
- console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
184
+ console.print(
185
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
186
+ )
144
187
  console.print("Run 'golf init <project_name>' to create a new project.")
145
- track_event("cli_build_failed", {"success": False, "environment": "prod"})
188
+ track_event(
189
+ "cli_build_failed",
190
+ {
191
+ "success": False,
192
+ "environment": "prod",
193
+ "error_type": "NoProjectFound",
194
+ "error_message": "No GolfMCP project found",
195
+ },
196
+ )
146
197
  raise typer.Exit(code=1)
147
-
198
+
148
199
  # Load settings from the found project
149
200
  settings = load_settings(project_root)
150
-
201
+
151
202
  # Set default output directory if not specified
152
- if output_dir is None:
153
- output_dir = project_root / "dist"
154
- else:
155
- output_dir = Path(output_dir)
156
-
203
+ output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
204
+
157
205
  try:
158
206
  # Build the project without copying environment variables
159
207
  from golf.commands.build import build_project
160
- build_project(project_root, settings, output_dir, build_env="prod", copy_env=False)
208
+
209
+ build_project(
210
+ project_root, settings, output_dir, build_env="prod", copy_env=False
211
+ )
161
212
  # Track successful build with environment
162
213
  track_event("cli_build_success", {"success": True, "environment": "prod"})
163
- except Exception:
164
- track_event("cli_build_failed", {"success": False, "environment": "prod"})
214
+ except Exception as e:
215
+ error_type = type(e).__name__
216
+ error_message = str(e)
217
+ track_event(
218
+ "cli_build_failed",
219
+ {
220
+ "success": False,
221
+ "environment": "prod",
222
+ "error_type": error_type,
223
+ "error_message": error_message,
224
+ },
225
+ )
165
226
  raise
166
227
 
167
228
 
168
229
  @app.command()
169
230
  def run(
170
- dist_dir: Optional[str] = typer.Option(
231
+ dist_dir: str | None = typer.Option(
171
232
  None, "--dist-dir", "-d", help="Directory containing the built server"
172
233
  ),
173
- host: Optional[str] = typer.Option(
234
+ host: str | None = typer.Option(
174
235
  None, "--host", "-h", help="Host to bind to (overrides settings)"
175
236
  ),
176
- port: Optional[int] = typer.Option(
237
+ port: int | None = typer.Option(
177
238
  None, "--port", "-p", help="Port to bind to (overrides settings)"
178
239
  ),
179
240
  build_first: bool = typer.Option(
180
241
  True, "--build/--no-build", help="Build the project before running"
181
242
  ),
182
- ):
243
+ ) -> None:
183
244
  """Run the built FastMCP server.
184
-
245
+
185
246
  This command runs the built server from the dist directory.
186
247
  By default, it will build the project first if needed.
187
248
  """
188
249
  # Find project root directory
189
250
  project_root, config_path = find_project_root()
190
-
251
+
191
252
  if not project_root:
192
- console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
253
+ console.print(
254
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
255
+ )
193
256
  console.print("Run 'golf init <project_name>' to create a new project.")
194
- track_event("cli_run_failed", {"success": False})
257
+ track_event(
258
+ "cli_run_failed",
259
+ {
260
+ "success": False,
261
+ "error_type": "NoProjectFound",
262
+ "error_message": "No GolfMCP project found",
263
+ },
264
+ )
195
265
  raise typer.Exit(code=1)
196
-
266
+
197
267
  # Load settings from the found project
198
268
  settings = load_settings(project_root)
199
-
269
+
200
270
  # Set default dist directory if not specified
201
- if dist_dir is None:
202
- dist_dir = project_root / "dist"
203
- else:
204
- dist_dir = Path(dist_dir)
205
-
271
+ dist_dir = project_root / "dist" if dist_dir is None else Path(dist_dir)
272
+
206
273
  # Check if dist directory exists
207
274
  if not dist_dir.exists():
208
275
  if build_first:
209
- console.print(f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]")
210
- # Build the project
211
- from golf.commands.build import build_project
212
- build_project(project_root, settings, dist_dir)
276
+ console.print(
277
+ f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]"
278
+ )
279
+ try:
280
+ # Build the project
281
+ from golf.commands.build import build_project
282
+
283
+ build_project(project_root, settings, dist_dir)
284
+ except Exception as e:
285
+ error_type = type(e).__name__
286
+ error_message = str(e)
287
+ console.print(
288
+ f"[bold red]Error building project:[/bold red] {error_message}"
289
+ )
290
+ track_event(
291
+ "cli_run_failed",
292
+ {
293
+ "success": False,
294
+ "error_type": f"BuildError.{error_type}",
295
+ "error_message": error_message,
296
+ },
297
+ )
298
+ raise
213
299
  else:
214
- console.print(f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]")
215
- console.print("Run 'golf build' first or use --build to build automatically.")
216
- track_event("cli_run_failed", {"success": False})
300
+ console.print(
301
+ f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]"
302
+ )
303
+ console.print(
304
+ "Run 'golf build' first or use --build to build automatically."
305
+ )
306
+ track_event(
307
+ "cli_run_failed",
308
+ {
309
+ "success": False,
310
+ "error_type": "DistNotFound",
311
+ "error_message": "Dist directory not found",
312
+ },
313
+ )
217
314
  raise typer.Exit(code=1)
218
-
315
+
219
316
  try:
220
317
  # Import and run the server
221
318
  from golf.commands.run import run_server
319
+
222
320
  return_code = run_server(
223
321
  project_path=project_root,
224
322
  settings=settings,
225
323
  dist_dir=dist_dir,
226
324
  host=host,
227
- port=port
325
+ port=port,
228
326
  )
229
-
327
+
230
328
  # Track based on return code
231
329
  if return_code == 0:
232
330
  track_event("cli_run_success", {"success": True})
233
331
  else:
234
- track_event("cli_run_failed", {"success": False})
235
-
332
+ track_event(
333
+ "cli_run_failed",
334
+ {
335
+ "success": False,
336
+ "error_type": "NonZeroExit",
337
+ "error_message": f"Server exited with code {return_code}",
338
+ },
339
+ )
340
+
236
341
  # Exit with the same code as the server
237
342
  if return_code != 0:
238
343
  raise typer.Exit(code=return_code)
239
- except Exception:
240
- track_event("cli_run_failed", {"success": False})
344
+ except Exception as e:
345
+ error_type = type(e).__name__
346
+ error_message = str(e)
347
+ track_event(
348
+ "cli_run_failed",
349
+ {
350
+ "success": False,
351
+ "error_type": error_type,
352
+ "error_message": error_message,
353
+ },
354
+ )
241
355
  raise
242
356
 
243
357
 
244
358
  # Add telemetry command group
245
359
  @app.command()
246
360
  def telemetry(
247
- action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'")
248
- ):
361
+ action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'"),
362
+ ) -> None:
249
363
  """Manage telemetry settings."""
250
364
  if action.lower() == "enable":
251
365
  set_telemetry_enabled(True, persist=True)
252
- console.print("[green]✓[/green] Telemetry enabled. Thank you for helping improve Golf!")
366
+ console.print(
367
+ "[green]✓[/green] Telemetry enabled. Thank you for helping improve Golf!"
368
+ )
253
369
  elif action.lower() == "disable":
254
370
  set_telemetry_enabled(False, persist=True)
255
- console.print("[yellow]Telemetry disabled.[/yellow] You can re-enable it anytime with: golf telemetry enable")
371
+ console.print(
372
+ "[yellow]Telemetry disabled.[/yellow] You can re-enable it anytime with: golf telemetry enable"
373
+ )
256
374
  else:
257
- console.print(f"[red]Unknown action '{action}'. Use 'enable' or 'disable'.[/red]")
375
+ console.print(
376
+ f"[red]Unknown action '{action}'. Use 'enable' or 'disable'.[/red]"
377
+ )
258
378
  raise typer.Exit(code=1)
259
379
 
260
380
 
@@ -267,13 +387,13 @@ if __name__ == "__main__":
267
387
  border_style="green",
268
388
  )
269
389
  )
270
-
390
+
271
391
  # Add telemetry notice if enabled
272
392
  if is_telemetry_enabled():
273
393
  console.print(
274
394
  "[dim]📊 Anonymous usage data is collected to improve Golf. "
275
395
  "Disable with: golf telemetry disable[/dim]\n"
276
396
  )
277
-
397
+
278
398
  # Run the CLI app
279
- app()
399
+ 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