golf-mcp 0.1.0__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 (41) hide show
  1. golf/__init__.py +1 -0
  2. golf/auth/__init__.py +109 -0
  3. golf/auth/helpers.py +56 -0
  4. golf/auth/oauth.py +798 -0
  5. golf/auth/provider.py +110 -0
  6. golf/cli/__init__.py +1 -0
  7. golf/cli/main.py +223 -0
  8. golf/commands/__init__.py +3 -0
  9. golf/commands/build.py +78 -0
  10. golf/commands/init.py +197 -0
  11. golf/commands/run.py +68 -0
  12. golf/core/__init__.py +1 -0
  13. golf/core/builder.py +1169 -0
  14. golf/core/builder_auth.py +157 -0
  15. golf/core/builder_telemetry.py +208 -0
  16. golf/core/config.py +205 -0
  17. golf/core/parser.py +509 -0
  18. golf/core/transformer.py +168 -0
  19. golf/examples/__init__.py +1 -0
  20. golf/examples/basic/.env +3 -0
  21. golf/examples/basic/.env.example +3 -0
  22. golf/examples/basic/README.md +117 -0
  23. golf/examples/basic/golf.json +9 -0
  24. golf/examples/basic/pre_build.py +28 -0
  25. golf/examples/basic/prompts/welcome.py +30 -0
  26. golf/examples/basic/resources/current_time.py +41 -0
  27. golf/examples/basic/resources/info.py +27 -0
  28. golf/examples/basic/resources/weather/common.py +48 -0
  29. golf/examples/basic/resources/weather/current.py +32 -0
  30. golf/examples/basic/resources/weather/forecast.py +32 -0
  31. golf/examples/basic/tools/github_user.py +67 -0
  32. golf/examples/basic/tools/hello.py +29 -0
  33. golf/examples/basic/tools/payments/charge.py +50 -0
  34. golf/examples/basic/tools/payments/common.py +34 -0
  35. golf/examples/basic/tools/payments/refund.py +50 -0
  36. golf_mcp-0.1.0.dist-info/METADATA +78 -0
  37. golf_mcp-0.1.0.dist-info/RECORD +41 -0
  38. golf_mcp-0.1.0.dist-info/WHEEL +5 -0
  39. golf_mcp-0.1.0.dist-info/entry_points.txt +2 -0
  40. golf_mcp-0.1.0.dist-info/licenses/LICENSE +201 -0
  41. golf_mcp-0.1.0.dist-info/top_level.txt +1 -0
golf/auth/provider.py ADDED
@@ -0,0 +1,110 @@
1
+ """OAuth provider configuration for GolfMCP authentication.
2
+
3
+ This module defines the ProviderConfig class used to configure
4
+ OAuth authentication for GolfMCP servers.
5
+ """
6
+
7
+ from typing import Dict, List, Optional, Any
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+
11
+ class ProviderConfig(BaseModel):
12
+ """Configuration for an OAuth2 provider.
13
+
14
+ This class defines the configuration for an OAuth2 provider,
15
+ including the endpoints, credentials, and other settings needed
16
+ to authenticate with the provider.
17
+ """
18
+
19
+ # Provider identification
20
+ provider: str = Field(
21
+ ...,
22
+ description="Provider type (e.g., 'github', 'google', 'custom')"
23
+ )
24
+
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
+
29
+ # These fields will store the actual values read at runtime in dist/server.py
30
+ # 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)")
33
+
34
+ # OAuth endpoints (can be baked in)
35
+ authorize_url: str = Field(..., description="Authorization endpoint URL")
36
+ 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)"
40
+ )
41
+
42
+ jwks_uri: Optional[str] = Field(
43
+ None,
44
+ description="JSON Web Key Set URI (for token validation)"
45
+ )
46
+
47
+ scopes: List[str] = Field(
48
+ default_factory=list,
49
+ description="OAuth scopes to request from the provider"
50
+ )
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"
55
+ )
56
+
57
+ callback_path: str = Field(
58
+ "/auth/callback",
59
+ description="Path on this server where the IdP should redirect after authentication"
60
+ )
61
+
62
+ # 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)")
65
+ token_expiration: int = Field(
66
+ 3600,
67
+ description="JWT token expiration time in seconds",
68
+ ge=60,
69
+ le=86400
70
+ )
71
+
72
+ settings: Dict[str, Any] = Field(
73
+ default_factory=dict,
74
+ description="Additional provider-specific settings"
75
+ )
76
+
77
+ @field_validator('provider')
78
+ @classmethod
79
+ def validate_provider(cls, value: str) -> str:
80
+ """Validate the provider type.
81
+
82
+ Ensures the provider type is a valid, supported provider.
83
+
84
+ Args:
85
+ value: The provider type
86
+
87
+ Returns:
88
+ The validated provider type
89
+
90
+ Raises:
91
+ ValueError: If the provider type is not supported
92
+ """
93
+ known_providers = {'custom', 'github', 'google', 'jwks'}
94
+
95
+ if value not in known_providers and not value.startswith('custom:'):
96
+ raise ValueError(
97
+ f"Unknown provider: '{value}'. Must be one of {known_providers} "
98
+ "or start with 'custom:'"
99
+ )
100
+ return value
101
+
102
+ def get_provider_name(self) -> str:
103
+ """Get a clean provider name for display purposes.
104
+
105
+ Returns:
106
+ A human-readable provider name
107
+ """
108
+ if self.provider.startswith('custom:'):
109
+ return self.provider[7:] # Remove 'custom:' prefix
110
+ return self.provider.capitalize()
golf/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """CLI package for the GolfMCP framework."""
golf/cli/main.py ADDED
@@ -0,0 +1,223 @@
1
+ """CLI entry points for GolfMCP."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+
11
+ from golf import __version__
12
+ from golf.core.config import load_settings, find_project_root
13
+
14
+ # Create console for rich output
15
+ console = Console()
16
+
17
+ # Create the typer app instance
18
+ app = typer.Typer(
19
+ name="golf",
20
+ help="GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate",
21
+ add_completion=False,
22
+ )
23
+
24
+
25
+ def _version_callback(value: bool) -> None:
26
+ """Print version and exit if --version flag is used."""
27
+ if value:
28
+ console.print(f"GolfMCP v{__version__}")
29
+ raise typer.Exit()
30
+
31
+
32
+ @app.callback()
33
+ def callback(
34
+ version: bool = typer.Option(
35
+ None,
36
+ "--version",
37
+ "-V",
38
+ help="Show the version and exit.",
39
+ callback=_version_callback,
40
+ is_eager=True,
41
+ ),
42
+ verbose: bool = typer.Option(
43
+ False, "--verbose", "-v", help="Increase verbosity of output."
44
+ ),
45
+ no_telemetry: bool = typer.Option(
46
+ False, "--no-telemetry", help="Disable telemetry collection."
47
+ ),
48
+ ) -> None:
49
+ """GolfMCP: A Pythonic framework for building MCP servers with zero boilerplate."""
50
+ # Set verbosity in environment for other components to access
51
+ if verbose:
52
+ os.environ["GOLF_VERBOSE"] = "1"
53
+
54
+ # Set telemetry flag
55
+ if no_telemetry:
56
+ os.environ["GOLF_TELEMETRY"] = "0"
57
+
58
+
59
+ @app.command()
60
+ def init(
61
+ project_name: str = typer.Argument(..., help="Name of the project to create"),
62
+ output_dir: Optional[Path] = typer.Option(
63
+ None, "--output-dir", "-o", help="Directory to create the project in"
64
+ ),
65
+ template: str = typer.Option(
66
+ "basic", "--template", "-t", help="Template to use (basic or advanced)"
67
+ ),
68
+ ) -> None:
69
+ """Initialize a new GolfMCP project.
70
+
71
+ Creates a new directory with the project scaffold, including
72
+ examples for tools, resources, and prompts.
73
+ """
74
+ # Import here to avoid circular imports
75
+ from golf.commands.init import initialize_project
76
+
77
+ # Use the current directory if no output directory is specified
78
+ if output_dir is None:
79
+ output_dir = Path.cwd() / project_name
80
+
81
+ # Execute the initialization command
82
+ initialize_project(project_name=project_name, output_dir=output_dir, template=template)
83
+
84
+
85
+ # Create a build group with subcommands
86
+ build_app = typer.Typer(help="Build a standalone FastMCP application")
87
+ app.add_typer(build_app, name="build")
88
+
89
+
90
+ @build_app.command("dev")
91
+ def build_dev(
92
+ output_dir: Optional[str] = typer.Option(
93
+ None, "--output-dir", "-o", help="Directory to output the built project"
94
+ )
95
+ ):
96
+ """Build a development version with environment variables copied."""
97
+ # Find project root directory
98
+ project_root, config_path = find_project_root()
99
+
100
+ if not project_root:
101
+ console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
102
+ console.print("Run 'golf init <project_name>' to create a new project.")
103
+ raise typer.Exit(code=1)
104
+
105
+ # Load settings from the found project
106
+ settings = load_settings(project_root)
107
+
108
+ # Set default output directory if not specified
109
+ if output_dir is None:
110
+ output_dir = project_root / "dist"
111
+ else:
112
+ output_dir = Path(output_dir)
113
+
114
+ # Build the project with environment variables copied
115
+ from golf.commands.build import build_project
116
+ build_project(project_root, settings, output_dir, build_env="dev", copy_env=True)
117
+
118
+
119
+ @build_app.command("prod")
120
+ def build_prod(
121
+ output_dir: Optional[str] = typer.Option(
122
+ None, "--output-dir", "-o", help="Directory to output the built project"
123
+ )
124
+ ):
125
+ """Build a production version without copying environment variables."""
126
+ # Find project root directory
127
+ project_root, config_path = find_project_root()
128
+
129
+ if not project_root:
130
+ console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
131
+ console.print("Run 'golf init <project_name>' to create a new project.")
132
+ raise typer.Exit(code=1)
133
+
134
+ # Load settings from the found project
135
+ settings = load_settings(project_root)
136
+
137
+ # Set default output directory if not specified
138
+ if output_dir is None:
139
+ output_dir = project_root / "dist"
140
+ else:
141
+ output_dir = Path(output_dir)
142
+
143
+ # Build the project without copying environment variables
144
+ from golf.commands.build import build_project
145
+ build_project(project_root, settings, output_dir, build_env="prod", copy_env=False)
146
+
147
+
148
+ @app.command()
149
+ def run(
150
+ dist_dir: Optional[str] = typer.Option(
151
+ None, "--dist-dir", "-d", help="Directory containing the built server"
152
+ ),
153
+ host: Optional[str] = typer.Option(
154
+ None, "--host", "-h", help="Host to bind to (overrides settings)"
155
+ ),
156
+ port: Optional[int] = typer.Option(
157
+ None, "--port", "-p", help="Port to bind to (overrides settings)"
158
+ ),
159
+ build_first: bool = typer.Option(
160
+ True, "--build/--no-build", help="Build the project before running"
161
+ ),
162
+ ):
163
+ """Run the built FastMCP server.
164
+
165
+ This command runs the built server from the dist directory.
166
+ By default, it will build the project first if needed.
167
+ """
168
+ # Find project root directory
169
+ project_root, config_path = find_project_root()
170
+
171
+ if not project_root:
172
+ console.print("[bold red]Error: No GolfMCP project found in the current directory or any parent directory.[/bold red]")
173
+ console.print("Run 'golf init <project_name>' to create a new project.")
174
+ raise typer.Exit(code=1)
175
+
176
+ # Load settings from the found project
177
+ settings = load_settings(project_root)
178
+
179
+ # Set default dist directory if not specified
180
+ if dist_dir is None:
181
+ dist_dir = project_root / "dist"
182
+ else:
183
+ dist_dir = Path(dist_dir)
184
+
185
+ # Check if dist directory exists
186
+ if not dist_dir.exists():
187
+ if build_first:
188
+ console.print(f"[yellow]Dist directory {dist_dir} not found. Building first...[/yellow]")
189
+ # Build the project
190
+ from golf.commands.build import build_project
191
+ build_project(project_root, settings, dist_dir)
192
+ else:
193
+ console.print(f"[bold red]Error: Dist directory {dist_dir} not found.[/bold red]")
194
+ console.print("Run 'golf build' first or use --build to build automatically.")
195
+ raise typer.Exit(code=1)
196
+
197
+ # Import and run the server
198
+ from golf.commands.run import run_server
199
+ return_code = run_server(
200
+ project_path=project_root,
201
+ settings=settings,
202
+ dist_dir=dist_dir,
203
+ host=host,
204
+ port=port
205
+ )
206
+
207
+ # Exit with the same code as the server
208
+ if return_code != 0:
209
+ raise typer.Exit(code=return_code)
210
+
211
+
212
+ if __name__ == "__main__":
213
+ # Show welcome banner when run directly
214
+ console.print(
215
+ Panel.fit(
216
+ f"[bold green]GolfMCP[/bold green] v{__version__}\n"
217
+ "[dim]A Pythonic framework for building MCP servers[/dim]",
218
+ border_style="green",
219
+ )
220
+ )
221
+
222
+ # Run the CLI app
223
+ app()
@@ -0,0 +1,3 @@
1
+ """GolfMCP command implementations."""
2
+
3
+ from golf.commands import init, build, run
golf/commands/build.py ADDED
@@ -0,0 +1,78 @@
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
+ from pathlib import Path
8
+ import argparse
9
+
10
+ from rich.console import Console
11
+
12
+ from golf.core.config import Settings, load_settings
13
+ from golf.core.builder import build_project as core_build_project
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 executed directly
39
+ if __name__ == "__main__":
40
+ parser = argparse.ArgumentParser(description="Build a standalone FastMCP application")
41
+ parser.add_argument(
42
+ "--project-path", "-p",
43
+ type=Path,
44
+ default=Path.cwd(),
45
+ help="Path to the project root (default: current directory)"
46
+ )
47
+ parser.add_argument(
48
+ "--output-dir", "-o",
49
+ type=Path,
50
+ default=Path.cwd() / "dist",
51
+ help="Directory to output the built project (default: ./dist)"
52
+ )
53
+ parser.add_argument(
54
+ "--build-env",
55
+ type=str,
56
+ default="prod",
57
+ choices=["dev", "prod"],
58
+ help="Build environment to use (default: prod)"
59
+ )
60
+ parser.add_argument(
61
+ "--copy-env",
62
+ action="store_true",
63
+ help="Copy environment variables to the built application"
64
+ )
65
+
66
+ args = parser.parse_args()
67
+
68
+ # Load settings from the project path
69
+ settings = load_settings(args.project_path)
70
+
71
+ # Execute the build
72
+ 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
+ )
golf/commands/init.py ADDED
@@ -0,0 +1,197 @@
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
+ console = Console()
11
+
12
+
13
+ def initialize_project(
14
+ project_name: str,
15
+ output_dir: Path,
16
+ template: str = "basic",
17
+ ) -> None:
18
+ """Initialize a new GolfMCP project with the specified template.
19
+
20
+ Args:
21
+ project_name: Name of the project
22
+ output_dir: Directory where the project will be created
23
+ template: Template to use (basic or advanced)
24
+ """
25
+ # Validate template
26
+ if template not in ("basic", "advanced"):
27
+ console.print(f"[bold red]Error:[/bold red] Unknown template '{template}'")
28
+ console.print("Available templates: basic, advanced")
29
+ return
30
+
31
+ # Check if directory exists
32
+ if output_dir.exists():
33
+ if not output_dir.is_dir():
34
+ console.print(
35
+ f"[bold red]Error:[/bold red] '{output_dir}' exists but is not a directory."
36
+ )
37
+ return
38
+
39
+ # Check if directory is empty
40
+ if any(output_dir.iterdir()):
41
+ if not Confirm.ask(
42
+ f"Directory '{output_dir}' is not empty. Continue anyway?",
43
+ default=False,
44
+ ):
45
+ console.print("Initialization cancelled.")
46
+ return
47
+ else:
48
+ # Create the directory
49
+ output_dir.mkdir(parents=True)
50
+
51
+ # Find template directory within the installed package
52
+ import golf
53
+ package_init_file = Path(golf.__file__)
54
+ # The 'examples' directory is now inside the 'golf' package directory
55
+ # e.g. golf/examples/basic, so go up one from __init__.py to get to 'golf'
56
+ template_dir = package_init_file.parent / "examples" / template
57
+
58
+ if not template_dir.exists():
59
+ console.print(
60
+ f"[bold red]Error:[/bold red] Could not find template '{template}'"
61
+ )
62
+ return
63
+
64
+ # Copy template files
65
+ with Progress(
66
+ SpinnerColumn(),
67
+ TextColumn("[bold green]Creating project structure...[/bold green]"),
68
+ transient=True,
69
+ ) as progress:
70
+ progress.add_task("copying", total=None)
71
+
72
+ # Copy directory structure
73
+ _copy_template(template_dir, output_dir, project_name)
74
+
75
+ # Create virtual environment
76
+ console.print("[bold green]Project initialized successfully![/bold green]")
77
+ console.print(f"\nTo get started, run:")
78
+ console.print(f" cd {output_dir.name}")
79
+ console.print(f" golf build dev")
80
+
81
+
82
+ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> None:
83
+ """Copy template files to the target directory, with variable substitution.
84
+
85
+ Args:
86
+ source_dir: Source template directory
87
+ target_dir: Target project directory
88
+ project_name: Name of the project (for substitutions)
89
+ """
90
+ # Create standard directory structure
91
+ (target_dir / "tools").mkdir(exist_ok=True)
92
+ (target_dir / "resources").mkdir(exist_ok=True)
93
+ (target_dir / "prompts").mkdir(exist_ok=True)
94
+
95
+ # Copy all files from the template
96
+ for source_path in source_dir.glob("**/*"):
97
+ # Skip if directory (we'll create directories as needed)
98
+ if source_path.is_dir():
99
+ continue
100
+
101
+ # Compute relative path
102
+ rel_path = source_path.relative_to(source_dir)
103
+ target_path = target_dir / rel_path
104
+
105
+ # Create parent directories if needed
106
+ target_path.parent.mkdir(parents=True, exist_ok=True)
107
+
108
+ # Copy and substitute content for text files
109
+ if _is_text_file(source_path):
110
+ with open(source_path, "r", encoding="utf-8") as f:
111
+ content = f.read()
112
+
113
+ # Replace template variables
114
+ content = content.replace("{{project_name}}", project_name)
115
+ content = content.replace("{{project_name_lowercase}}", project_name.lower())
116
+
117
+ with open(target_path, "w", encoding="utf-8") as f:
118
+ f.write(content)
119
+ else:
120
+ # Binary file, just copy
121
+ shutil.copy2(source_path, target_path)
122
+
123
+ # Create .env file
124
+ env_file = target_dir / ".env"
125
+ with open(env_file, "w", encoding="utf-8") as f:
126
+ f.write(f"GOLF_NAME={project_name}\n")
127
+ f.write("GOLF_HOST=127.0.0.1\n")
128
+ f.write("GOLF_PORT=3000\n")
129
+
130
+ # Create a .gitignore if it doesn't exist
131
+ gitignore_file = target_dir / ".gitignore"
132
+ if not gitignore_file.exists():
133
+ with open(gitignore_file, "w", encoding="utf-8") as f:
134
+ f.write("# Python\n")
135
+ f.write("__pycache__/\n")
136
+ f.write("*.py[cod]\n")
137
+ f.write("*$py.class\n")
138
+ f.write("*.so\n")
139
+ f.write(".Python\n")
140
+ f.write("env/\n")
141
+ f.write("build/\n")
142
+ f.write("develop-eggs/\n")
143
+ f.write("dist/\n")
144
+ f.write("downloads/\n")
145
+ f.write("eggs/\n")
146
+ f.write(".eggs/\n")
147
+ f.write("lib/\n")
148
+ f.write("lib64/\n")
149
+ f.write("parts/\n")
150
+ f.write("sdist/\n")
151
+ f.write("var/\n")
152
+ f.write("*.egg-info/\n")
153
+ f.write(".installed.cfg\n")
154
+ f.write("*.egg\n\n")
155
+ f.write("# Environment\n")
156
+ f.write(".env\n")
157
+ f.write(".venv\n")
158
+ f.write("env/\n")
159
+ f.write("venv/\n")
160
+ f.write("ENV/\n")
161
+ f.write("env.bak/\n")
162
+ f.write("venv.bak/\n\n")
163
+ f.write("# GolfMCP\n")
164
+ f.write(".golf/\n")
165
+ f.write("dist/\n")
166
+
167
+
168
+ def _is_text_file(path: Path) -> bool:
169
+ """Check if a file is a text file that needs variable substitution.
170
+
171
+ Args:
172
+ path: Path to check
173
+
174
+ Returns:
175
+ True if the file is a text file
176
+ """
177
+ # List of known text file extensions
178
+ text_extensions = {
179
+ ".py", ".md", ".txt", ".html", ".css", ".js", ".json",
180
+ ".yml", ".yaml", ".toml", ".ini", ".cfg", ".env", ".example"
181
+ }
182
+
183
+ # Check if the file has a text extension
184
+ if path.suffix in text_extensions:
185
+ return True
186
+
187
+ # Check specific filenames without extensions
188
+ if path.name in {".gitignore", "README", "LICENSE"}:
189
+ return True
190
+
191
+ # Try to detect if it's a text file by reading a bit of it
192
+ try:
193
+ with open(path, "r", encoding="utf-8") as f:
194
+ f.read(1024)
195
+ return True
196
+ except UnicodeDecodeError:
197
+ return False
golf/commands/run.py ADDED
@@ -0,0 +1,68 @@
1
+ """Command to run the built FastMCP server."""
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from rich.console import Console
10
+
11
+ from golf.core.config import Settings
12
+
13
+ console = Console()
14
+
15
+
16
+ def run_server(
17
+ project_path: Path,
18
+ settings: Settings,
19
+ dist_dir: Optional[Path] = None,
20
+ host: Optional[str] = None,
21
+ port: Optional[int] = None,
22
+ ) -> int:
23
+ """Run the built FastMCP server.
24
+
25
+ Args:
26
+ project_path: Path to the project root
27
+ settings: Project settings
28
+ dist_dir: Path to the directory containing the built server (defaults to project_path/dist)
29
+ host: Host to bind the server to (overrides settings)
30
+ port: Port to bind the server to (overrides settings)
31
+
32
+ Returns:
33
+ Process return code
34
+ """
35
+ # Set default dist directory if not specified
36
+ if dist_dir is None:
37
+ dist_dir = project_path / "dist"
38
+
39
+ # Check if server file exists
40
+ server_path = dist_dir / "server.py"
41
+ if not server_path.exists():
42
+ console.print(f"[bold red]Error: Server file {server_path} not found.[/bold red]")
43
+ return 1
44
+
45
+ # Prepare environment variables
46
+ env = os.environ.copy()
47
+ if host is not None:
48
+ env["HOST"] = host
49
+ elif settings.host:
50
+ env["HOST"] = settings.host
51
+
52
+ if port is not None:
53
+ env["PORT"] = str(port)
54
+ elif settings.port:
55
+ env["PORT"] = str(settings.port)
56
+
57
+ # Run the server
58
+ try:
59
+ # Using subprocess to properly handle signals (Ctrl+C)
60
+ process = subprocess.run(
61
+ [sys.executable, str(server_path)],
62
+ cwd=dist_dir,
63
+ env=env,
64
+ )
65
+ return process.returncode
66
+ except KeyboardInterrupt:
67
+ console.print("\n[yellow]Server stopped by user[/yellow]")
68
+ return 0
golf/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Core functionality for the GolfMCP framework."""