golf-mcp 0.2.16__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.
Files changed (52) hide show
  1. golf/__init__.py +1 -0
  2. golf/auth/__init__.py +277 -0
  3. golf/auth/api_key.py +73 -0
  4. golf/auth/factory.py +360 -0
  5. golf/auth/helpers.py +175 -0
  6. golf/auth/providers.py +586 -0
  7. golf/auth/registry.py +256 -0
  8. golf/cli/__init__.py +1 -0
  9. golf/cli/branding.py +191 -0
  10. golf/cli/main.py +377 -0
  11. golf/commands/__init__.py +5 -0
  12. golf/commands/build.py +81 -0
  13. golf/commands/init.py +290 -0
  14. golf/commands/run.py +137 -0
  15. golf/core/__init__.py +1 -0
  16. golf/core/builder.py +1884 -0
  17. golf/core/builder_auth.py +209 -0
  18. golf/core/builder_metrics.py +221 -0
  19. golf/core/builder_telemetry.py +99 -0
  20. golf/core/config.py +199 -0
  21. golf/core/parser.py +1085 -0
  22. golf/core/telemetry.py +492 -0
  23. golf/core/transformer.py +231 -0
  24. golf/examples/__init__.py +0 -0
  25. golf/examples/basic/.env.example +4 -0
  26. golf/examples/basic/README.md +133 -0
  27. golf/examples/basic/auth.py +76 -0
  28. golf/examples/basic/golf.json +5 -0
  29. golf/examples/basic/prompts/welcome.py +27 -0
  30. golf/examples/basic/resources/current_time.py +34 -0
  31. golf/examples/basic/resources/info.py +28 -0
  32. golf/examples/basic/resources/weather/city.py +46 -0
  33. golf/examples/basic/resources/weather/client.py +48 -0
  34. golf/examples/basic/resources/weather/current.py +36 -0
  35. golf/examples/basic/resources/weather/forecast.py +36 -0
  36. golf/examples/basic/tools/calculator.py +94 -0
  37. golf/examples/basic/tools/say/hello.py +65 -0
  38. golf/metrics/__init__.py +10 -0
  39. golf/metrics/collector.py +320 -0
  40. golf/metrics/registry.py +12 -0
  41. golf/telemetry/__init__.py +23 -0
  42. golf/telemetry/instrumentation.py +1402 -0
  43. golf/utilities/__init__.py +12 -0
  44. golf/utilities/context.py +53 -0
  45. golf/utilities/elicitation.py +170 -0
  46. golf/utilities/sampling.py +221 -0
  47. golf_mcp-0.2.16.dist-info/METADATA +262 -0
  48. golf_mcp-0.2.16.dist-info/RECORD +52 -0
  49. golf_mcp-0.2.16.dist-info/WHEEL +5 -0
  50. golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
  51. golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
  52. golf_mcp-0.2.16.dist-info/top_level.txt +1 -0
golf/cli/main.py ADDED
@@ -0,0 +1,377 @@
1
+ """CLI entry points for GolfMCP."""
2
+
3
+ import atexit
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from golf import __version__
11
+ from golf.cli.branding import create_welcome_banner, create_command_header
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
+ )
20
+
21
+ # Create console for rich output
22
+ console = Console()
23
+
24
+ # Create the typer app instance
25
+ app = typer.Typer(
26
+ name="golf",
27
+ help="GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate",
28
+ add_completion=False,
29
+ )
30
+
31
+ # Register telemetry shutdown on exit
32
+ atexit.register(shutdown)
33
+
34
+
35
+ def _version_callback(value: bool) -> None:
36
+ """Print version and exit if --version flag is used."""
37
+ if value:
38
+ create_welcome_banner(__version__, console)
39
+ raise typer.Exit()
40
+
41
+
42
+ @app.callback()
43
+ def callback(
44
+ version: bool = typer.Option(
45
+ None,
46
+ "--version",
47
+ "-V",
48
+ help="Show the version and exit.",
49
+ callback=_version_callback,
50
+ is_eager=True,
51
+ ),
52
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Increase verbosity of output."),
53
+ no_telemetry: bool = typer.Option(
54
+ False,
55
+ "--no-telemetry",
56
+ help="Disable telemetry collection (persists for future commands).",
57
+ ),
58
+ test: bool = typer.Option(
59
+ False,
60
+ "--test",
61
+ hidden=True,
62
+ help="Run in test mode (disables telemetry for this execution only).",
63
+ ),
64
+ ) -> None:
65
+ """GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate."""
66
+ # Set verbosity in environment for other components to access
67
+ if verbose:
68
+ os.environ["GOLF_VERBOSE"] = "1"
69
+
70
+ # Set test mode if flag is used (temporary, just for this execution)
71
+ if test:
72
+ set_telemetry_enabled(False, persist=False)
73
+ os.environ["GOLF_TEST_MODE"] = "1"
74
+
75
+ # Set telemetry preference if flag is used (permanent)
76
+ if no_telemetry:
77
+ set_telemetry_enabled(False, persist=True)
78
+ console.print("[dim]Telemetry has been disabled. You can re-enable it with: golf telemetry enable[/dim]")
79
+
80
+
81
+ @app.command()
82
+ def init(
83
+ project_name: str = typer.Argument(..., help="Name of the project to create"),
84
+ output_dir: Path | None = typer.Option(None, "--output-dir", "-o", help="Directory to create the project in"),
85
+ ) -> None:
86
+ """Initialize a new GolfMCP project.
87
+
88
+ Creates a new directory with the project scaffold, including
89
+ examples for tools, resources, and prompts.
90
+ """
91
+ # Show the Golf logo for project initialization
92
+ create_welcome_banner(__version__, console)
93
+ console.print()
94
+ create_command_header("Initialize Project", f"Creating {project_name}", console)
95
+
96
+ # Import here to avoid circular imports
97
+ from golf.commands.init import initialize_project
98
+
99
+ # Use the current directory if no output directory is specified
100
+ if output_dir is None:
101
+ output_dir = Path.cwd() / project_name
102
+
103
+ # Execute the initialization command (it handles its own tracking)
104
+ initialize_project(project_name=project_name, output_dir=output_dir)
105
+
106
+
107
+ # Create a build group with subcommands
108
+ build_app = typer.Typer(help="Build a standalone FastMCP application")
109
+ app.add_typer(build_app, name="build")
110
+
111
+
112
+ @build_app.command("dev")
113
+ def build_dev(
114
+ output_dir: str | None = typer.Option(None, "--output-dir", "-o", help="Directory to output the built project"),
115
+ ) -> None:
116
+ """Build a development version with app environment variables copied.
117
+
118
+ Golf credentials (GOLF_*) are always loaded from .env for build operations.
119
+ All environment variables are copied to the built project for development.
120
+ """
121
+ # Find project root directory
122
+ project_root, config_path = find_project_root()
123
+
124
+ if not project_root:
125
+ console.print(
126
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
127
+ )
128
+ console.print("Run 'golf init <project_name>' to create a new project.")
129
+ track_event(
130
+ "cli_build_failed",
131
+ {
132
+ "success": False,
133
+ "environment": "dev",
134
+ "error_type": "NoProjectFound",
135
+ "error_message": "No GolfMCP project found",
136
+ },
137
+ )
138
+ raise typer.Exit(code=1)
139
+
140
+ # Load settings from the found project
141
+ settings = load_settings(project_root)
142
+
143
+ # Set default output directory if not specified
144
+ output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
145
+
146
+ try:
147
+ # Build the project with environment variables copied
148
+ from golf.commands.build import build_project
149
+
150
+ build_project(project_root, settings, output_dir, build_env="dev", copy_env=True)
151
+ # Track successful build with environment
152
+ track_event("cli_build_success", {"success": True, "environment": "dev"})
153
+ except Exception as e:
154
+ track_detailed_error(
155
+ "cli_build_failed",
156
+ e,
157
+ context="Development build with environment variables",
158
+ operation="build_dev",
159
+ additional_props={"environment": "dev", "copy_env": True},
160
+ )
161
+ raise
162
+
163
+
164
+ @build_app.command("prod")
165
+ def build_prod(
166
+ output_dir: str | None = typer.Option(None, "--output-dir", "-o", help="Directory to output the built project"),
167
+ ) -> None:
168
+ """Build a production version for deployment.
169
+
170
+ Environment variables from .env are loaded for build operations.
171
+ App environment variables are NOT copied for security - provide them
172
+ in your deployment environment.
173
+ """
174
+ # Find project root directory
175
+ project_root, config_path = find_project_root()
176
+
177
+ if not project_root:
178
+ console.print(
179
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
180
+ )
181
+ console.print("Run 'golf init <project_name>' to create a new project.")
182
+ track_event(
183
+ "cli_build_failed",
184
+ {
185
+ "success": False,
186
+ "environment": "prod",
187
+ "error_type": "NoProjectFound",
188
+ "error_message": "No GolfMCP project found",
189
+ },
190
+ )
191
+ raise typer.Exit(code=1)
192
+
193
+ # Load settings from the found project
194
+ settings = load_settings(project_root)
195
+
196
+ # Set default output directory if not specified
197
+ output_dir = project_root / "dist" if output_dir is None else Path(output_dir)
198
+
199
+ try:
200
+ # Build the project without copying environment variables
201
+ from golf.commands.build import build_project
202
+
203
+ build_project(project_root, settings, output_dir, build_env="prod", copy_env=False)
204
+ # Track successful build with environment
205
+ track_event("cli_build_success", {"success": True, "environment": "prod"})
206
+ except Exception as e:
207
+ track_detailed_error(
208
+ "cli_build_failed",
209
+ e,
210
+ context="Production build without environment variables",
211
+ operation="build_prod",
212
+ additional_props={"environment": "prod", "copy_env": False},
213
+ )
214
+ raise
215
+
216
+
217
+ @app.command()
218
+ def run(
219
+ dist_dir: str | None = typer.Option(None, "--dist-dir", "-d", help="Directory containing the built server"),
220
+ host: str | None = typer.Option(None, "--host", "-h", help="Host to bind to (overrides settings)"),
221
+ port: int | None = typer.Option(None, "--port", "-p", help="Port to bind to (overrides settings)"),
222
+ build_first: bool = typer.Option(True, "--build/--no-build", help="Build the project before running"),
223
+ ) -> None:
224
+ """Run the built FastMCP server.
225
+
226
+ This command runs the built server from the dist directory.
227
+ By default, it will build the project first if needed.
228
+ """
229
+ # Find project root directory
230
+ project_root, config_path = find_project_root()
231
+
232
+ if not project_root:
233
+ console.print(
234
+ "[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]"
235
+ )
236
+ console.print("Run 'golf init <project_name>' to create a new project.")
237
+ track_event(
238
+ "cli_run_failed",
239
+ {
240
+ "success": False,
241
+ "error_type": "NoProjectFound",
242
+ "error_message": "No GolfMCP project found",
243
+ },
244
+ )
245
+ raise typer.Exit(code=1)
246
+
247
+ # Load settings from the found project
248
+ settings = load_settings(project_root)
249
+
250
+ # Set default dist directory if not specified
251
+ dist_dir = project_root / "dist" if dist_dir is None else Path(dist_dir)
252
+
253
+ # Check if dist directory exists
254
+ if not dist_dir.exists():
255
+ if build_first:
256
+ console.print(f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]")
257
+ try:
258
+ # Build the project
259
+ from golf.commands.build import build_project
260
+
261
+ build_project(project_root, settings, dist_dir)
262
+ except Exception as e:
263
+ console.print(f"[bold red]Error building project:[/bold red] {str(e)}")
264
+ track_detailed_error(
265
+ "cli_run_failed",
266
+ e,
267
+ context="Auto-build before running server",
268
+ operation="auto_build_before_run",
269
+ additional_props={"auto_build": True},
270
+ )
271
+ raise
272
+ else:
273
+ console.print(f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]")
274
+ console.print("Run 'golf build' first or use --build to build automatically.")
275
+ track_event(
276
+ "cli_run_failed",
277
+ {
278
+ "success": False,
279
+ "error_type": "DistNotFound",
280
+ "error_message": "Dist directory not found",
281
+ },
282
+ )
283
+ raise typer.Exit(code=1)
284
+
285
+ try:
286
+ # Import and run the server
287
+ from golf.commands.run import run_server
288
+
289
+ return_code = run_server(
290
+ project_path=project_root,
291
+ settings=settings,
292
+ dist_dir=dist_dir,
293
+ host=host,
294
+ port=port,
295
+ )
296
+
297
+ # Track based on return code with better categorization
298
+ if return_code == 0:
299
+ track_event("cli_run_success", {"success": True})
300
+ elif return_code in [130, 143, 137, 2]:
301
+ # Intentional shutdowns (not errors):
302
+ # 130: Ctrl+C (SIGINT)
303
+ # 143: SIGTERM (graceful shutdown, e.g., Kubernetes, Docker)
304
+ # 137: SIGKILL (forced shutdown)
305
+ # 2: General interrupt/graceful shutdown
306
+ shutdown_type = {
307
+ 130: "UserInterrupt",
308
+ 143: "GracefulShutdown",
309
+ 137: "ForcedShutdown",
310
+ 2: "Interrupt",
311
+ }.get(return_code, "GracefulShutdown")
312
+
313
+ track_event(
314
+ "cli_run_shutdown",
315
+ {
316
+ "success": True, # Not an error
317
+ "shutdown_type": shutdown_type,
318
+ "exit_code": return_code,
319
+ },
320
+ )
321
+ else:
322
+ # Actual errors (unexpected exit codes)
323
+ track_event(
324
+ "cli_run_failed",
325
+ {
326
+ "success": False,
327
+ "error_type": "UnexpectedExit",
328
+ "error_message": (f"Server process exited unexpectedly with code {return_code}"),
329
+ "exit_code": return_code,
330
+ "operation": "server_process_execution",
331
+ "context": "Server process terminated with unexpected exit code",
332
+ },
333
+ )
334
+
335
+ # Exit with the same code as the server
336
+ if return_code != 0:
337
+ raise typer.Exit(code=return_code)
338
+ except Exception as e:
339
+ track_detailed_error(
340
+ "cli_run_failed",
341
+ e,
342
+ context="Server execution or startup failure",
343
+ operation="run_server_execution",
344
+ additional_props={"has_dist_dir": dist_dir.exists() if dist_dir else False},
345
+ )
346
+ raise
347
+
348
+
349
+ # Add telemetry command group
350
+ @app.command()
351
+ def telemetry(
352
+ action: str = typer.Argument(..., help="Action to perform: 'enable' or 'disable'"),
353
+ ) -> None:
354
+ """Manage telemetry settings."""
355
+ if action.lower() == "enable":
356
+ set_telemetry_enabled(True, persist=True)
357
+ console.print("[green]✓[/green] Telemetry enabled. Thank you for helping improve Golf!")
358
+ elif action.lower() == "disable":
359
+ set_telemetry_enabled(False, persist=True)
360
+ console.print("[yellow]Telemetry disabled.[/yellow] You can re-enable it anytime with: golf telemetry enable")
361
+ else:
362
+ console.print(f"[red]Unknown action '{action}'. Use 'enable' or 'disable'.[/red]")
363
+ raise typer.Exit(code=1)
364
+
365
+
366
+ if __name__ == "__main__":
367
+ # Show welcome banner when run directly
368
+ create_welcome_banner(__version__, console)
369
+
370
+ # Add telemetry notice if enabled
371
+ if is_telemetry_enabled():
372
+ console.print(
373
+ "[dim]📊 Anonymous usage data is collected to improve Golf. Disable with: golf telemetry disable[/dim]\n"
374
+ )
375
+
376
+ # Run the CLI app
377
+ app()
@@ -0,0 +1,5 @@
1
+ """GolfMCP command implementations."""
2
+
3
+ from golf.commands import build, init, run
4
+
5
+ __all__ = ["build", "init", "run"]
golf/commands/build.py ADDED
@@ -0,0 +1,81 @@
1
+ """Build command for GolfMCP.
2
+
3
+ This module implements the `golf build` command which generates a standalone
4
+ FastMCP application from a GolfMCP project.
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from rich.console import Console
11
+
12
+ from golf.core.builder import build_project as core_build_project
13
+ from golf.core.config import Settings, load_settings
14
+
15
+ console = Console()
16
+
17
+
18
+ def build_project(
19
+ project_path: Path,
20
+ settings: Settings,
21
+ output_dir: Path,
22
+ build_env: str = "prod",
23
+ copy_env: bool = False,
24
+ ) -> None:
25
+ """Build a standalone FastMCP application from a GolfMCP project.
26
+
27
+ Args:
28
+ project_path: Path to the project root
29
+ settings: Project settings
30
+ output_dir: Directory to output the built project
31
+ build_env: Build environment ('dev' or 'prod')
32
+ copy_env: Whether to copy environment variables to the built app
33
+ """
34
+ # Call the centralized build function from core.builder
35
+ core_build_project(project_path, settings, output_dir, build_env=build_env, copy_env=copy_env)
36
+
37
+
38
+ # Add a main section to run the build_project function when this module is
39
+ # executed directly
40
+ if __name__ == "__main__":
41
+ parser = argparse.ArgumentParser(description="Build a standalone FastMCP application")
42
+ parser.add_argument(
43
+ "--project-path",
44
+ "-p",
45
+ type=Path,
46
+ default=Path.cwd(),
47
+ help="Path to the project root (default: current directory)",
48
+ )
49
+ parser.add_argument(
50
+ "--output-dir",
51
+ "-o",
52
+ type=Path,
53
+ default=Path.cwd() / "dist",
54
+ help="Directory to output the built project (default: ./dist)",
55
+ )
56
+ parser.add_argument(
57
+ "--build-env",
58
+ type=str,
59
+ default="prod",
60
+ choices=["dev", "prod"],
61
+ help="Build environment to use (default: prod)",
62
+ )
63
+ parser.add_argument(
64
+ "--copy-env",
65
+ action="store_true",
66
+ help="Copy environment variables to the built application",
67
+ )
68
+
69
+ args = parser.parse_args()
70
+
71
+ # Load settings from the project path
72
+ settings = load_settings(args.project_path)
73
+
74
+ # Execute the build
75
+ build_project(
76
+ args.project_path,
77
+ settings,
78
+ args.output_dir,
79
+ build_env=args.build_env,
80
+ copy_env=args.copy_env,
81
+ )