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.
- golf/__init__.py +1 -0
- golf/auth/__init__.py +109 -0
- golf/auth/helpers.py +56 -0
- golf/auth/oauth.py +798 -0
- golf/auth/provider.py +110 -0
- golf/cli/__init__.py +1 -0
- golf/cli/main.py +223 -0
- golf/commands/__init__.py +3 -0
- golf/commands/build.py +78 -0
- golf/commands/init.py +197 -0
- golf/commands/run.py +68 -0
- golf/core/__init__.py +1 -0
- golf/core/builder.py +1169 -0
- golf/core/builder_auth.py +157 -0
- golf/core/builder_telemetry.py +208 -0
- golf/core/config.py +205 -0
- golf/core/parser.py +509 -0
- golf/core/transformer.py +168 -0
- golf/examples/__init__.py +1 -0
- golf/examples/basic/.env +3 -0
- golf/examples/basic/.env.example +3 -0
- golf/examples/basic/README.md +117 -0
- golf/examples/basic/golf.json +9 -0
- golf/examples/basic/pre_build.py +28 -0
- golf/examples/basic/prompts/welcome.py +30 -0
- golf/examples/basic/resources/current_time.py +41 -0
- golf/examples/basic/resources/info.py +27 -0
- golf/examples/basic/resources/weather/common.py +48 -0
- golf/examples/basic/resources/weather/current.py +32 -0
- golf/examples/basic/resources/weather/forecast.py +32 -0
- golf/examples/basic/tools/github_user.py +67 -0
- golf/examples/basic/tools/hello.py +29 -0
- golf/examples/basic/tools/payments/charge.py +50 -0
- golf/examples/basic/tools/payments/common.py +34 -0
- golf/examples/basic/tools/payments/refund.py +50 -0
- golf_mcp-0.1.0.dist-info/METADATA +78 -0
- golf_mcp-0.1.0.dist-info/RECORD +41 -0
- golf_mcp-0.1.0.dist-info/WHEEL +5 -0
- golf_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- golf_mcp-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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()
|
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."""
|