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/commands/build.py CHANGED
@@ -4,26 +4,26 @@ This module implements the `golf build` command which generates a standalone
4
4
  FastMCP application from a GolfMCP project.
5
5
  """
6
6
 
7
- from pathlib import Path
8
7
  import argparse
8
+ from pathlib import Path
9
9
 
10
10
  from rich.console import Console
11
11
 
12
- from golf.core.config import Settings, load_settings
13
12
  from golf.core.builder import build_project as core_build_project
13
+ from golf.core.config import Settings, load_settings
14
14
 
15
15
  console = Console()
16
16
 
17
17
 
18
18
  def build_project(
19
- project_path: Path,
20
- settings: Settings,
19
+ project_path: Path,
20
+ settings: Settings,
21
21
  output_dir: Path,
22
22
  build_env: str = "prod",
23
- copy_env: bool = False
23
+ copy_env: bool = False,
24
24
  ) -> None:
25
25
  """Build a standalone FastMCP application from a GolfMCP project.
26
-
26
+
27
27
  Args:
28
28
  project_path: Path to the project root
29
29
  settings: Project settings
@@ -32,47 +32,53 @@ def build_project(
32
32
  copy_env: Whether to copy environment variables to the built app
33
33
  """
34
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)
35
+ core_build_project(
36
+ project_path, settings, output_dir, build_env=build_env, copy_env=copy_env
37
+ )
36
38
 
37
39
 
38
40
  # Add a main section to run the build_project function when this module is executed directly
39
41
  if __name__ == "__main__":
40
- parser = argparse.ArgumentParser(description="Build a standalone FastMCP application")
42
+ parser = argparse.ArgumentParser(
43
+ description="Build a standalone FastMCP application"
44
+ )
41
45
  parser.add_argument(
42
- "--project-path", "-p",
43
- type=Path,
46
+ "--project-path",
47
+ "-p",
48
+ type=Path,
44
49
  default=Path.cwd(),
45
- help="Path to the project root (default: current directory)"
50
+ help="Path to the project root (default: current directory)",
46
51
  )
47
52
  parser.add_argument(
48
- "--output-dir", "-o",
49
- type=Path,
53
+ "--output-dir",
54
+ "-o",
55
+ type=Path,
50
56
  default=Path.cwd() / "dist",
51
- help="Directory to output the built project (default: ./dist)"
57
+ help="Directory to output the built project (default: ./dist)",
52
58
  )
53
59
  parser.add_argument(
54
60
  "--build-env",
55
61
  type=str,
56
62
  default="prod",
57
63
  choices=["dev", "prod"],
58
- help="Build environment to use (default: prod)"
64
+ help="Build environment to use (default: prod)",
59
65
  )
60
66
  parser.add_argument(
61
67
  "--copy-env",
62
68
  action="store_true",
63
- help="Copy environment variables to the built application"
69
+ help="Copy environment variables to the built application",
64
70
  )
65
-
71
+
66
72
  args = parser.parse_args()
67
-
73
+
68
74
  # Load settings from the project path
69
75
  settings = load_settings(args.project_path)
70
-
76
+
71
77
  # Execute the build
72
78
  build_project(
73
- args.project_path,
74
- settings,
75
- args.output_dir,
76
- build_env=args.build_env,
77
- copy_env=args.copy_env
78
- )
79
+ args.project_path,
80
+ settings,
81
+ args.output_dir,
82
+ build_env=args.build_env,
83
+ copy_env=args.copy_env,
84
+ )
golf/commands/init.py CHANGED
@@ -1,15 +1,13 @@
1
1
  """Project initialization command implementation."""
2
2
 
3
- import os
4
3
  import shutil
5
4
  from pathlib import Path
6
- from typing import Optional
7
5
 
8
6
  from rich.console import Console
9
7
  from rich.progress import Progress, SpinnerColumn, TextColumn
10
8
  from rich.prompt import Confirm
11
9
 
12
- from golf.core.telemetry import track_event
10
+ from golf.core.telemetry import track_command, track_event
13
11
 
14
12
  console = Console()
15
13
 
@@ -20,83 +18,110 @@ def initialize_project(
20
18
  template: str = "basic",
21
19
  ) -> None:
22
20
  """Initialize a new GolfMCP project with the specified template.
23
-
21
+
24
22
  Args:
25
23
  project_name: Name of the project
26
24
  output_dir: Directory where the project will be created
27
25
  template: Template to use (basic or api_key)
28
26
  """
29
- # Validate template
30
- valid_templates = ("basic", "api_key")
31
- if template not in valid_templates:
32
- console.print(f"[bold red]Error:[/bold red] Unknown template '{template}'")
33
- console.print(f"Available templates: {', '.join(valid_templates)}")
34
- track_event("cli_init_failed", {"success": False})
35
- return
36
-
37
- # Check if directory exists
38
- if output_dir.exists():
39
- if not output_dir.is_dir():
40
- console.print(
41
- f"[bold red]Error:[/bold red] '{output_dir}' exists but is not a directory."
27
+ try:
28
+ # Validate template
29
+ valid_templates = ("basic", "api_key")
30
+ if template not in valid_templates:
31
+ console.print(f"[bold red]Error:[/bold red] Unknown template '{template}'")
32
+ console.print(f"Available templates: {', '.join(valid_templates)}")
33
+ track_command(
34
+ "init",
35
+ success=False,
36
+ error_type="InvalidTemplate",
37
+ error_message=f"Unknown template: {template}",
42
38
  )
43
- track_event("cli_init_failed", {"success": False})
44
39
  return
45
-
46
- # Check if directory is empty
47
- if any(output_dir.iterdir()):
48
- if not Confirm.ask(
40
+
41
+ # Check if directory exists
42
+ if output_dir.exists():
43
+ if not output_dir.is_dir():
44
+ console.print(
45
+ f"[bold red]Error:[/bold red] '{output_dir}' exists but is not a directory."
46
+ )
47
+ track_command(
48
+ "init",
49
+ success=False,
50
+ error_type="NotADirectory",
51
+ error_message="Target exists but is not a directory",
52
+ )
53
+ return
54
+
55
+ # Check if directory is empty
56
+ if any(output_dir.iterdir()) and not Confirm.ask(
49
57
  f"Directory '{output_dir}' is not empty. Continue anyway?",
50
58
  default=False,
51
59
  ):
52
60
  console.print("Initialization cancelled.")
53
61
  track_event("cli_init_cancelled", {"success": False})
54
62
  return
55
- else:
56
- # Create the directory
57
- output_dir.mkdir(parents=True)
58
-
59
- # Find template directory within the installed package
60
- import golf
61
- package_init_file = Path(golf.__file__)
62
- # The 'examples' directory is now inside the 'golf' package directory
63
- # e.g. golf/examples/basic, so go up one from __init__.py to get to 'golf'
64
- template_dir = package_init_file.parent / "examples" / template
65
-
66
- if not template_dir.exists():
63
+ else:
64
+ # Create the directory
65
+ output_dir.mkdir(parents=True)
66
+
67
+ # Find template directory within the installed package
68
+ import golf
69
+
70
+ package_init_file = Path(golf.__file__)
71
+ # The 'examples' directory is now inside the 'golf' package directory
72
+ # e.g. golf/examples/basic, so go up one from __init__.py to get to 'golf'
73
+ template_dir = package_init_file.parent / "examples" / template
74
+
75
+ if not template_dir.exists():
76
+ console.print(
77
+ f"[bold red]Error:[/bold red] Could not find template '{template}'"
78
+ )
79
+ track_command(
80
+ "init",
81
+ success=False,
82
+ error_type="TemplateNotFound",
83
+ error_message=f"Template directory not found: {template}",
84
+ )
85
+ return
86
+
87
+ # Copy template files
88
+ with Progress(
89
+ SpinnerColumn(),
90
+ TextColumn("[bold green]Creating project structure...[/bold green]"),
91
+ transient=True,
92
+ ) as progress:
93
+ progress.add_task("copying", total=None)
94
+
95
+ # Copy directory structure
96
+ _copy_template(template_dir, output_dir, project_name)
97
+
98
+ # Create virtual environment
99
+ console.print("[bold green]Project initialized successfully![/bold green]")
100
+ console.print("\nTo get started, run:")
101
+ console.print(f" cd {output_dir.name}")
102
+ console.print(" golf build dev")
103
+
104
+ # Track successful initialization
105
+ track_event("cli_init_success", {"success": True, "template": template})
106
+ except Exception as e:
107
+ # Capture error details for telemetry
108
+ error_type = type(e).__name__
109
+ error_message = str(e)
110
+
67
111
  console.print(
68
- f"[bold red]Error:[/bold red] Could not find template '{template}'"
112
+ f"[bold red]Error during initialization:[/bold red] {error_message}"
113
+ )
114
+ track_command(
115
+ "init", success=False, error_type=error_type, error_message=error_message
69
116
  )
70
- track_event("cli_init_failed", {"success": False})
71
- return
72
-
73
- # Copy template files
74
- with Progress(
75
- SpinnerColumn(),
76
- TextColumn("[bold green]Creating project structure...[/bold green]"),
77
- transient=True,
78
- ) as progress:
79
- progress.add_task("copying", total=None)
80
-
81
- # Copy directory structure
82
- _copy_template(template_dir, output_dir, project_name)
83
-
84
- # Create virtual environment
85
- console.print("[bold green]Project initialized successfully![/bold green]")
86
- console.print(f"\nTo get started, run:")
87
- console.print(f" cd {output_dir.name}")
88
- console.print(f" golf build dev")
89
-
90
- # Track successful initialization
91
- track_event("cli_init_success", {
92
- "success": True,
93
- "template": template
94
- })
117
+
118
+ # Re-raise to maintain existing behavior
119
+ raise
95
120
 
96
121
 
97
122
  def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> None:
98
123
  """Copy template files to the target directory, with variable substitution.
99
-
124
+
100
125
  Args:
101
126
  source_dir: Source template directory
102
127
  target_dir: Target project directory
@@ -106,42 +131,44 @@ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> Non
106
131
  (target_dir / "tools").mkdir(exist_ok=True)
107
132
  (target_dir / "resources").mkdir(exist_ok=True)
108
133
  (target_dir / "prompts").mkdir(exist_ok=True)
109
-
134
+
110
135
  # Copy all files from the template
111
136
  for source_path in source_dir.glob("**/*"):
112
137
  # Skip if directory (we'll create directories as needed)
113
138
  if source_path.is_dir():
114
139
  continue
115
-
140
+
116
141
  # Compute relative path
117
142
  rel_path = source_path.relative_to(source_dir)
118
143
  target_path = target_dir / rel_path
119
-
144
+
120
145
  # Create parent directories if needed
121
146
  target_path.parent.mkdir(parents=True, exist_ok=True)
122
-
147
+
123
148
  # Copy and substitute content for text files
124
149
  if _is_text_file(source_path):
125
- with open(source_path, "r", encoding="utf-8") as f:
150
+ with open(source_path, encoding="utf-8") as f:
126
151
  content = f.read()
127
-
152
+
128
153
  # Replace template variables
129
154
  content = content.replace("{{project_name}}", project_name)
130
- content = content.replace("{{project_name_lowercase}}", project_name.lower())
131
-
155
+ content = content.replace(
156
+ "{{project_name_lowercase}}", project_name.lower()
157
+ )
158
+
132
159
  with open(target_path, "w", encoding="utf-8") as f:
133
160
  f.write(content)
134
161
  else:
135
162
  # Binary file, just copy
136
163
  shutil.copy2(source_path, target_path)
137
-
164
+
138
165
  # Create .env file
139
166
  env_file = target_dir / ".env"
140
167
  with open(env_file, "w", encoding="utf-8") as f:
141
168
  f.write(f"GOLF_NAME={project_name}\n")
142
169
  f.write("GOLF_HOST=127.0.0.1\n")
143
170
  f.write("GOLF_PORT=3000\n")
144
-
171
+
145
172
  # Create a .gitignore if it doesn't exist
146
173
  gitignore_file = target_dir / ".gitignore"
147
174
  if not gitignore_file.exists():
@@ -182,31 +209,43 @@ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> Non
182
209
 
183
210
  def _is_text_file(path: Path) -> bool:
184
211
  """Check if a file is a text file that needs variable substitution.
185
-
212
+
186
213
  Args:
187
214
  path: Path to check
188
-
215
+
189
216
  Returns:
190
217
  True if the file is a text file
191
218
  """
192
219
  # List of known text file extensions
193
220
  text_extensions = {
194
- ".py", ".md", ".txt", ".html", ".css", ".js", ".json",
195
- ".yml", ".yaml", ".toml", ".ini", ".cfg", ".env", ".example"
221
+ ".py",
222
+ ".md",
223
+ ".txt",
224
+ ".html",
225
+ ".css",
226
+ ".js",
227
+ ".json",
228
+ ".yml",
229
+ ".yaml",
230
+ ".toml",
231
+ ".ini",
232
+ ".cfg",
233
+ ".env",
234
+ ".example",
196
235
  }
197
-
236
+
198
237
  # Check if the file has a text extension
199
238
  if path.suffix in text_extensions:
200
239
  return True
201
-
240
+
202
241
  # Check specific filenames without extensions
203
242
  if path.name in {".gitignore", "README", "LICENSE"}:
204
243
  return True
205
-
244
+
206
245
  # Try to detect if it's a text file by reading a bit of it
207
246
  try:
208
- with open(path, "r", encoding="utf-8") as f:
247
+ with open(path, encoding="utf-8") as f:
209
248
  f.read(1024)
210
249
  return True
211
250
  except UnicodeDecodeError:
212
- return False
251
+ return False
golf/commands/run.py CHANGED
@@ -4,7 +4,6 @@ import os
4
4
  import subprocess
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import Optional
8
7
 
9
8
  from rich.console import Console
10
9
 
@@ -16,44 +15,46 @@ console = Console()
16
15
  def run_server(
17
16
  project_path: Path,
18
17
  settings: Settings,
19
- dist_dir: Optional[Path] = None,
20
- host: Optional[str] = None,
21
- port: Optional[int] = None,
18
+ dist_dir: Path | None = None,
19
+ host: str | None = None,
20
+ port: int | None = None,
22
21
  ) -> int:
23
22
  """Run the built FastMCP server.
24
-
23
+
25
24
  Args:
26
25
  project_path: Path to the project root
27
26
  settings: Project settings
28
27
  dist_dir: Path to the directory containing the built server (defaults to project_path/dist)
29
28
  host: Host to bind the server to (overrides settings)
30
29
  port: Port to bind the server to (overrides settings)
31
-
30
+
32
31
  Returns:
33
32
  Process return code
34
33
  """
35
34
  # Set default dist directory if not specified
36
35
  if dist_dir is None:
37
36
  dist_dir = project_path / "dist"
38
-
37
+
39
38
  # Check if server file exists
40
39
  server_path = dist_dir / "server.py"
41
40
  if not server_path.exists():
42
- console.print(f"[bold red]Error: Server file {server_path} not found.[/bold red]")
41
+ console.print(
42
+ f"[bold red]Error: Server file {server_path} not found.[/bold red]"
43
+ )
43
44
  return 1
44
-
45
+
45
46
  # Prepare environment variables
46
47
  env = os.environ.copy()
47
48
  if host is not None:
48
49
  env["HOST"] = host
49
50
  elif settings.host:
50
51
  env["HOST"] = settings.host
51
-
52
+
52
53
  if port is not None:
53
54
  env["PORT"] = str(port)
54
55
  elif settings.port:
55
56
  env["PORT"] = str(settings.port)
56
-
57
+
57
58
  # Run the server
58
59
  try:
59
60
  # Using subprocess to properly handle signals (Ctrl+C)
@@ -62,11 +63,11 @@ def run_server(
62
63
  cwd=dist_dir,
63
64
  env=env,
64
65
  )
65
-
66
+
66
67
  return process.returncode
67
68
  except KeyboardInterrupt:
68
69
  console.print("\n[yellow]Server stopped by user[/yellow]")
69
70
  return 0
70
71
  except Exception as e:
71
72
  console.print(f"\n[bold red]Error running server:[/bold red] {e}")
72
- return 1
73
+ return 1
golf/core/__init__.py CHANGED
@@ -1 +1 @@
1
- """Core functionality for the GolfMCP framework."""
1
+ """Core functionality for the GolfMCP framework."""