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/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, track_command
10
+ from golf.core.telemetry import track_command, track_event
13
11
 
14
12
  console = Console()
15
13
 
@@ -20,7 +18,7 @@ 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
@@ -32,45 +30,60 @@ def initialize_project(
32
30
  if template not in valid_templates:
33
31
  console.print(f"[bold red]Error:[/bold red] Unknown template '{template}'")
34
32
  console.print(f"Available templates: {', '.join(valid_templates)}")
35
- track_command("init", success=False, error_type="InvalidTemplate", error_message=f"Unknown template: {template}")
33
+ track_command(
34
+ "init",
35
+ success=False,
36
+ error_type="InvalidTemplate",
37
+ error_message=f"Unknown template: {template}",
38
+ )
36
39
  return
37
-
40
+
38
41
  # Check if directory exists
39
42
  if output_dir.exists():
40
43
  if not output_dir.is_dir():
41
44
  console.print(
42
45
  f"[bold red]Error:[/bold red] '{output_dir}' exists but is not a directory."
43
46
  )
44
- track_command("init", success=False, error_type="NotADirectory", error_message="Target exists but is not a directory")
47
+ track_command(
48
+ "init",
49
+ success=False,
50
+ error_type="NotADirectory",
51
+ error_message="Target exists but is not a directory",
52
+ )
45
53
  return
46
-
54
+
47
55
  # Check if directory is empty
48
- if any(output_dir.iterdir()):
49
- if not Confirm.ask(
50
- f"Directory '{output_dir}' is not empty. Continue anyway?",
51
- default=False,
52
- ):
53
- console.print("Initialization cancelled.")
54
- track_event("cli_init_cancelled", {"success": False})
55
- return
56
+ if any(output_dir.iterdir()) and not Confirm.ask(
57
+ f"Directory '{output_dir}' is not empty. Continue anyway?",
58
+ default=False,
59
+ ):
60
+ console.print("Initialization cancelled.")
61
+ track_event("cli_init_cancelled", {"success": False})
62
+ return
56
63
  else:
57
64
  # Create the directory
58
65
  output_dir.mkdir(parents=True)
59
-
66
+
60
67
  # Find template directory within the installed package
61
68
  import golf
69
+
62
70
  package_init_file = Path(golf.__file__)
63
71
  # The 'examples' directory is now inside the 'golf' package directory
64
72
  # e.g. golf/examples/basic, so go up one from __init__.py to get to 'golf'
65
73
  template_dir = package_init_file.parent / "examples" / template
66
-
74
+
67
75
  if not template_dir.exists():
68
76
  console.print(
69
77
  f"[bold red]Error:[/bold red] Could not find template '{template}'"
70
78
  )
71
- track_command("init", success=False, error_type="TemplateNotFound", error_message=f"Template directory not found: {template}")
79
+ track_command(
80
+ "init",
81
+ success=False,
82
+ error_type="TemplateNotFound",
83
+ error_message=f"Template directory not found: {template}",
84
+ )
72
85
  return
73
-
86
+
74
87
  # Copy template files
75
88
  with Progress(
76
89
  SpinnerColumn(),
@@ -78,36 +91,37 @@ def initialize_project(
78
91
  transient=True,
79
92
  ) as progress:
80
93
  progress.add_task("copying", total=None)
81
-
94
+
82
95
  # Copy directory structure
83
96
  _copy_template(template_dir, output_dir, project_name)
84
-
97
+
85
98
  # Create virtual environment
86
99
  console.print("[bold green]Project initialized successfully![/bold green]")
87
- console.print(f"\nTo get started, run:")
100
+ console.print("\nTo get started, run:")
88
101
  console.print(f" cd {output_dir.name}")
89
- console.print(f" golf build dev")
90
-
102
+ console.print(" golf build dev")
103
+
91
104
  # Track successful initialization
92
- track_event("cli_init_success", {
93
- "success": True,
94
- "template": template
95
- })
105
+ track_event("cli_init_success", {"success": True, "template": template})
96
106
  except Exception as e:
97
107
  # Capture error details for telemetry
98
108
  error_type = type(e).__name__
99
109
  error_message = str(e)
100
-
101
- console.print(f"[bold red]Error during initialization:[/bold red] {error_message}")
102
- track_command("init", success=False, error_type=error_type, error_message=error_message)
103
-
110
+
111
+ console.print(
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
116
+ )
117
+
104
118
  # Re-raise to maintain existing behavior
105
119
  raise
106
120
 
107
121
 
108
122
  def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> None:
109
123
  """Copy template files to the target directory, with variable substitution.
110
-
124
+
111
125
  Args:
112
126
  source_dir: Source template directory
113
127
  target_dir: Target project directory
@@ -117,42 +131,44 @@ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> Non
117
131
  (target_dir / "tools").mkdir(exist_ok=True)
118
132
  (target_dir / "resources").mkdir(exist_ok=True)
119
133
  (target_dir / "prompts").mkdir(exist_ok=True)
120
-
134
+
121
135
  # Copy all files from the template
122
136
  for source_path in source_dir.glob("**/*"):
123
137
  # Skip if directory (we'll create directories as needed)
124
138
  if source_path.is_dir():
125
139
  continue
126
-
140
+
127
141
  # Compute relative path
128
142
  rel_path = source_path.relative_to(source_dir)
129
143
  target_path = target_dir / rel_path
130
-
144
+
131
145
  # Create parent directories if needed
132
146
  target_path.parent.mkdir(parents=True, exist_ok=True)
133
-
147
+
134
148
  # Copy and substitute content for text files
135
149
  if _is_text_file(source_path):
136
- with open(source_path, "r", encoding="utf-8") as f:
150
+ with open(source_path, encoding="utf-8") as f:
137
151
  content = f.read()
138
-
152
+
139
153
  # Replace template variables
140
154
  content = content.replace("{{project_name}}", project_name)
141
- content = content.replace("{{project_name_lowercase}}", project_name.lower())
142
-
155
+ content = content.replace(
156
+ "{{project_name_lowercase}}", project_name.lower()
157
+ )
158
+
143
159
  with open(target_path, "w", encoding="utf-8") as f:
144
160
  f.write(content)
145
161
  else:
146
162
  # Binary file, just copy
147
163
  shutil.copy2(source_path, target_path)
148
-
164
+
149
165
  # Create .env file
150
166
  env_file = target_dir / ".env"
151
167
  with open(env_file, "w", encoding="utf-8") as f:
152
168
  f.write(f"GOLF_NAME={project_name}\n")
153
169
  f.write("GOLF_HOST=127.0.0.1\n")
154
170
  f.write("GOLF_PORT=3000\n")
155
-
171
+
156
172
  # Create a .gitignore if it doesn't exist
157
173
  gitignore_file = target_dir / ".gitignore"
158
174
  if not gitignore_file.exists():
@@ -193,31 +209,43 @@ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> Non
193
209
 
194
210
  def _is_text_file(path: Path) -> bool:
195
211
  """Check if a file is a text file that needs variable substitution.
196
-
212
+
197
213
  Args:
198
214
  path: Path to check
199
-
215
+
200
216
  Returns:
201
217
  True if the file is a text file
202
218
  """
203
219
  # List of known text file extensions
204
220
  text_extensions = {
205
- ".py", ".md", ".txt", ".html", ".css", ".js", ".json",
206
- ".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",
207
235
  }
208
-
236
+
209
237
  # Check if the file has a text extension
210
238
  if path.suffix in text_extensions:
211
239
  return True
212
-
240
+
213
241
  # Check specific filenames without extensions
214
242
  if path.name in {".gitignore", "README", "LICENSE"}:
215
243
  return True
216
-
244
+
217
245
  # Try to detect if it's a text file by reading a bit of it
218
246
  try:
219
- with open(path, "r", encoding="utf-8") as f:
247
+ with open(path, encoding="utf-8") as f:
220
248
  f.read(1024)
221
249
  return True
222
250
  except UnicodeDecodeError:
223
- 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,25 @@ def run_server(
62
63
  cwd=dist_dir,
63
64
  env=env,
64
65
  )
65
-
66
+
67
+ # Provide more context about the exit
68
+ if process.returncode == 0:
69
+ console.print("[green]Server stopped successfully[/green]")
70
+ elif process.returncode == 130:
71
+ console.print("[yellow]Server stopped by user interrupt (Ctrl+C)[/yellow]")
72
+ elif process.returncode == 143:
73
+ console.print("[yellow]Server stopped by SIGTERM (graceful shutdown)[/yellow]")
74
+ elif process.returncode == 137:
75
+ console.print("[yellow]Server stopped by SIGKILL (forced shutdown)[/yellow]")
76
+ elif process.returncode in [1, 2]:
77
+ console.print(f"[red]Server exited with error code {process.returncode}[/red]")
78
+ else:
79
+ console.print(f"[orange]Server exited with code {process.returncode}[/orange]")
80
+
66
81
  return process.returncode
67
82
  except KeyboardInterrupt:
68
- console.print("\n[yellow]Server stopped by user[/yellow]")
69
- return 0
83
+ console.print("\n[yellow]Server stopped by user (Ctrl+C)[/yellow]")
84
+ return 130 # Standard exit code for SIGINT
70
85
  except Exception as e:
71
86
  console.print(f"\n[bold red]Error running server:[/bold red] {e}")
72
- return 1
87
+ return 1
golf/core/__init__.py CHANGED
@@ -1 +1 @@
1
- """Core functionality for the GolfMCP framework."""
1
+ """Core functionality for the GolfMCP framework."""