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/commands/init.py ADDED
@@ -0,0 +1,290 @@
1
+ """Project initialization command implementation."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+ from rich.prompt import Confirm
9
+
10
+ from golf.cli.branding import (
11
+ create_success_message,
12
+ create_info_panel,
13
+ STATUS_ICONS,
14
+ GOLF_ORANGE,
15
+ )
16
+
17
+ from golf.core.telemetry import (
18
+ track_command,
19
+ track_event,
20
+ set_telemetry_enabled,
21
+ load_telemetry_preference,
22
+ )
23
+
24
+ console = Console()
25
+
26
+
27
+ def initialize_project(
28
+ project_name: str,
29
+ output_dir: Path,
30
+ ) -> None:
31
+ """Initialize a new GolfMCP project.
32
+
33
+ Args:
34
+ project_name: Name of the project
35
+ output_dir: Directory where the project will be created
36
+ """
37
+ try:
38
+ # Use the basic template by default
39
+ template = "basic"
40
+
41
+ # Check if directory exists
42
+ if output_dir.exists():
43
+ if not output_dir.is_dir():
44
+ console.print(f"[bold red]Error:[/bold red] '{output_dir}' exists but is not a directory.")
45
+ track_command(
46
+ "init",
47
+ success=False,
48
+ error_type="NotADirectory",
49
+ error_message="Target exists but is not a directory",
50
+ )
51
+ return
52
+
53
+ # Check if directory is empty
54
+ if any(output_dir.iterdir()) and not Confirm.ask(
55
+ f"Directory '{output_dir}' is not empty. Continue anyway?",
56
+ default=False,
57
+ ):
58
+ console.print("Initialization cancelled.")
59
+ track_event("cli_init_cancelled", {"success": False})
60
+ return
61
+ else:
62
+ # Create the directory
63
+ output_dir.mkdir(parents=True)
64
+
65
+ # Find template directory within the installed package
66
+ import golf
67
+
68
+ package_init_file = Path(golf.__file__)
69
+ # The 'examples' directory is now inside the 'golf' package directory
70
+ # e.g. golf/examples/basic, so go up one from __init__.py to get to 'golf'
71
+ template_dir = package_init_file.parent / "examples" / template
72
+
73
+ if not template_dir.exists():
74
+ console.print(f"[bold red]Error:[/bold red] Could not find template '{template}'")
75
+ track_command(
76
+ "init",
77
+ success=False,
78
+ error_type="TemplateNotFound",
79
+ error_message=f"Template directory not found: {template}",
80
+ )
81
+ return
82
+
83
+ # Copy template files
84
+ with Progress(
85
+ SpinnerColumn(),
86
+ TextColumn(
87
+ f"[bold {GOLF_ORANGE}]{STATUS_ICONS['building']} Creating project structure...[/bold {GOLF_ORANGE}]"
88
+ ),
89
+ transient=True,
90
+ ) as progress:
91
+ progress.add_task("copying", total=None)
92
+
93
+ # Copy directory structure
94
+ _copy_template(template_dir, output_dir, project_name)
95
+
96
+ # Ask for telemetry consent
97
+ _prompt_for_telemetry_consent()
98
+
99
+ # Show success message
100
+ console.print()
101
+ create_success_message("Project initialized successfully!", console)
102
+
103
+ # Show next steps
104
+ next_steps = f"cd {output_dir.name}\ngolf build dev\ngolf run"
105
+ create_info_panel("Next Steps", next_steps, console)
106
+
107
+ # Track successful initialization
108
+ track_event("cli_init_success", {"success": True, "template": template})
109
+ except Exception as e:
110
+ # Capture error details for telemetry
111
+ error_type = type(e).__name__
112
+ error_message = str(e)
113
+
114
+ console.print(f"[bold red]Error during initialization:[/bold red] {error_message}")
115
+ track_command("init", success=False, error_type=error_type, error_message=error_message)
116
+
117
+ # Re-raise to maintain existing behavior
118
+ raise
119
+
120
+
121
+ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> None:
122
+ """Copy template files to the target directory, with variable substitution.
123
+
124
+ Args:
125
+ source_dir: Source template directory
126
+ target_dir: Target project directory
127
+ project_name: Name of the project (for substitutions)
128
+ """
129
+ # Create standard directory structure
130
+ (target_dir / "tools").mkdir(exist_ok=True)
131
+ (target_dir / "resources").mkdir(exist_ok=True)
132
+ (target_dir / "prompts").mkdir(exist_ok=True)
133
+
134
+ # Copy all files from the template
135
+ for source_path in source_dir.glob("**/*"):
136
+ # Skip if directory (we'll create directories as needed)
137
+ if source_path.is_dir():
138
+ continue
139
+
140
+ # Compute relative path
141
+ rel_path = source_path.relative_to(source_dir)
142
+ target_path = target_dir / rel_path
143
+
144
+ # Create parent directories if needed
145
+ target_path.parent.mkdir(parents=True, exist_ok=True)
146
+
147
+ # Copy and substitute content for text files
148
+ if _is_text_file(source_path):
149
+ with open(source_path, encoding="utf-8") as f:
150
+ content = f.read()
151
+
152
+ # Replace template variables
153
+ content = content.replace("{{project_name}}", project_name)
154
+ content = content.replace("{{project_name_lowercase}}", project_name.lower())
155
+
156
+ with open(target_path, "w", encoding="utf-8") as f:
157
+ f.write(content)
158
+ else:
159
+ # Binary file, just copy
160
+ shutil.copy2(source_path, target_path)
161
+
162
+ # Create a .gitignore if it doesn't exist
163
+ gitignore_file = target_dir / ".gitignore"
164
+ if not gitignore_file.exists():
165
+ with open(gitignore_file, "w", encoding="utf-8") as f:
166
+ f.write("# Python\n")
167
+ f.write("__pycache__/\n")
168
+ f.write("*.py[cod]\n")
169
+ f.write("*$py.class\n")
170
+ f.write("*.so\n")
171
+ f.write(".Python\n")
172
+ f.write("env/\n")
173
+ f.write("build/\n")
174
+ f.write("develop-eggs/\n")
175
+ f.write("dist/\n")
176
+ f.write("downloads/\n")
177
+ f.write("eggs/\n")
178
+ f.write(".eggs/\n")
179
+ f.write("lib/\n")
180
+ f.write("lib64/\n")
181
+ f.write("parts/\n")
182
+ f.write("sdist/\n")
183
+ f.write("var/\n")
184
+ f.write("*.egg-info/\n")
185
+ f.write(".installed.cfg\n")
186
+ f.write("*.egg\n\n")
187
+ f.write("# Environment\n")
188
+ f.write(".env\n")
189
+ f.write(".venv\n")
190
+ f.write("env/\n")
191
+ f.write("venv/\n")
192
+ f.write("ENV/\n")
193
+ f.write("env.bak/\n")
194
+ f.write("venv.bak/\n\n")
195
+ f.write("# GolfMCP\n")
196
+ f.write(".golf/\n")
197
+ f.write("dist/\n")
198
+
199
+
200
+ def _prompt_for_telemetry_consent() -> None:
201
+ """Prompt user for telemetry consent and save their preference."""
202
+ import os
203
+
204
+ # Skip prompt in test mode, when telemetry is explicitly disabled, or if
205
+ # preference already exists
206
+ if os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on"):
207
+ return
208
+
209
+ # Skip if telemetry is explicitly disabled in environment
210
+ if os.environ.get("GOLF_TELEMETRY", "").lower() in ("0", "false", "no", "off"):
211
+ return
212
+
213
+ # Check if user already has a saved preference
214
+ existing_preference = load_telemetry_preference()
215
+ if existing_preference is not None:
216
+ return # User already made a choice
217
+
218
+ console.print()
219
+ console.rule("[bold blue]Anonymous usage analytics[/bold blue]", style="blue")
220
+ console.print()
221
+ console.print("Golf can collect [bold]anonymous usage analytics[/bold] to help improve the tool.")
222
+ console.print()
223
+ console.print("[dim]What we collect:[/dim]")
224
+ console.print(" • Command usage (init, build, run)")
225
+ console.print(" • Error types (to fix bugs)")
226
+ console.print(" • Golf version and Python version")
227
+ console.print(" • Operating system type")
228
+ console.print()
229
+ console.print("[dim]What we DON'T collect:[/dim]")
230
+ console.print(" • Your code or project content")
231
+ console.print(" • File paths or project names")
232
+ console.print(" • Personal information")
233
+ console.print(" • IP addresses")
234
+ console.print()
235
+ console.print("You can change this anytime by setting GOLF_TELEMETRY=0 in your environment.")
236
+ console.print()
237
+
238
+ enable_telemetry = Confirm.ask("[bold]Enable anonymous usage analytics?[/bold]", default=False)
239
+
240
+ set_telemetry_enabled(enable_telemetry, persist=True)
241
+
242
+ if enable_telemetry:
243
+ console.print("[green]✓[/green] Anonymous analytics enabled")
244
+ else:
245
+ console.print("[yellow]○[/yellow] Anonymous analytics disabled")
246
+ console.print()
247
+
248
+
249
+ def _is_text_file(path: Path) -> bool:
250
+ """Check if a file is a text file that needs variable substitution.
251
+
252
+ Args:
253
+ path: Path to check
254
+
255
+ Returns:
256
+ True if the file is a text file
257
+ """
258
+ # List of known text file extensions
259
+ text_extensions = {
260
+ ".py",
261
+ ".md",
262
+ ".txt",
263
+ ".html",
264
+ ".css",
265
+ ".js",
266
+ ".json",
267
+ ".yml",
268
+ ".yaml",
269
+ ".toml",
270
+ ".ini",
271
+ ".cfg",
272
+ ".env",
273
+ ".example",
274
+ }
275
+
276
+ # Check if the file has a text extension
277
+ if path.suffix in text_extensions:
278
+ return True
279
+
280
+ # Check specific filenames without extensions
281
+ if path.name in {".gitignore", "README", "LICENSE"}:
282
+ return True
283
+
284
+ # Try to detect if it's a text file by reading a bit of it
285
+ try:
286
+ with open(path, encoding="utf-8") as f:
287
+ f.read(1024)
288
+ return True
289
+ except UnicodeDecodeError:
290
+ return False
golf/commands/run.py ADDED
@@ -0,0 +1,137 @@
1
+ """Command to run the built FastMCP server."""
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console, Group
9
+ from rich.panel import Panel
10
+ from rich.align import Align
11
+ from rich.text import Text
12
+
13
+ from golf.cli.branding import create_command_header, get_status_text, STATUS_ICONS, GOLF_BLUE, GOLF_GREEN, GOLF_ORANGE
14
+ from golf.core.config import Settings
15
+
16
+ console = Console()
17
+
18
+
19
+ def run_server(
20
+ project_path: Path,
21
+ settings: Settings,
22
+ dist_dir: Path | None = None,
23
+ host: str | None = None,
24
+ port: int | None = None,
25
+ ) -> int:
26
+ """Run the built FastMCP server.
27
+
28
+ Args:
29
+ project_path: Path to the project root
30
+ settings: Project settings
31
+ dist_dir: Path to the directory containing the built server
32
+ (defaults to project_path/dist)
33
+ host: Host to bind the server to (overrides settings)
34
+ port: Port to bind the server to (overrides settings)
35
+
36
+ Returns:
37
+ Process return code
38
+ """
39
+ # Set default dist directory if not specified
40
+ if dist_dir is None:
41
+ dist_dir = project_path / "dist"
42
+
43
+ # Check if server file exists
44
+ server_path = dist_dir / "server.py"
45
+ if not server_path.exists():
46
+ console.print(get_status_text("error", f"Server file {server_path} not found"))
47
+ return 1
48
+
49
+ # Display server startup header
50
+ create_command_header("Starting Server", f"{settings.name}", console)
51
+
52
+ # Show server info with flashy styling
53
+ server_host = host or settings.host or "localhost"
54
+ server_port = port or settings.port or 3000
55
+
56
+ # Create server URL line
57
+ server_line = Text()
58
+ server_line.append("🚀 ", style=f"bold {GOLF_ORANGE}")
59
+ server_line.append(f"{STATUS_ICONS['server']} Server starting on ", style=f"bold {GOLF_BLUE}")
60
+ server_line.append(f"http://{server_host}:{server_port}", style=f"bold {GOLF_GREEN}")
61
+
62
+ # Create content with proper alignment
63
+ content_lines = [
64
+ "", # Empty line at top
65
+ Align.center(server_line),
66
+ ]
67
+
68
+ # Add telemetry status indicator
69
+ if settings.opentelemetry_enabled:
70
+ telemetry_line = Text("📊 OpenTelemetry enabled", style=f"dim {GOLF_BLUE}")
71
+ content_lines.append(Align.center(telemetry_line))
72
+
73
+ # Add empty line and stop instruction
74
+ content_lines.extend(
75
+ [
76
+ "", # Empty line before stop instruction
77
+ Align.center(Text("⚡ Press Ctrl+C to stop ⚡", style=f"dim {GOLF_ORANGE}")),
78
+ "", # Empty line at bottom
79
+ ]
80
+ )
81
+
82
+ console.print(
83
+ Panel(
84
+ Group(*content_lines),
85
+ border_style=GOLF_BLUE,
86
+ padding=(1, 2),
87
+ title="[bold]🌐 SERVER READY 🌐[/bold]",
88
+ title_align="center",
89
+ )
90
+ )
91
+ console.print()
92
+
93
+ # Prepare environment variables
94
+ env = os.environ.copy()
95
+ if host is not None:
96
+ env["HOST"] = host
97
+ elif settings.host:
98
+ env["HOST"] = settings.host
99
+
100
+ if port is not None:
101
+ env["PORT"] = str(port)
102
+ elif settings.port:
103
+ env["PORT"] = str(settings.port)
104
+
105
+ # Run the server
106
+ try:
107
+ # Using subprocess to properly handle signals (Ctrl+C)
108
+ process = subprocess.run(
109
+ [sys.executable, str(server_path)],
110
+ cwd=dist_dir,
111
+ env=env,
112
+ )
113
+
114
+ # Provide more context about the exit
115
+ console.print()
116
+ if process.returncode == 0:
117
+ console.print(get_status_text("success", "Server stopped successfully"))
118
+ elif process.returncode == 130:
119
+ console.print(get_status_text("info", "Server stopped by user interrupt (Ctrl+C)"))
120
+ elif process.returncode == 143:
121
+ console.print(get_status_text("info", "Server stopped by SIGTERM (graceful shutdown)"))
122
+ elif process.returncode == 137:
123
+ console.print(get_status_text("warning", "Server stopped by SIGKILL (forced shutdown)"))
124
+ elif process.returncode in [1, 2]:
125
+ console.print(get_status_text("error", f"Server exited with error code {process.returncode}"))
126
+ else:
127
+ console.print(get_status_text("warning", f"Server exited with code {process.returncode}"))
128
+
129
+ return process.returncode
130
+ except KeyboardInterrupt:
131
+ console.print()
132
+ console.print(get_status_text("info", "Server stopped by user (Ctrl+C)"))
133
+ return 130 # Standard exit code for SIGINT
134
+ except Exception as e:
135
+ console.print()
136
+ console.print(get_status_text("error", f"Error running server: {e}"))
137
+ return 1
golf/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Core functionality for the GolfMCP framework."""