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.
- golf/__init__.py +1 -0
- golf/auth/__init__.py +277 -0
- golf/auth/api_key.py +73 -0
- golf/auth/factory.py +360 -0
- golf/auth/helpers.py +175 -0
- golf/auth/providers.py +586 -0
- golf/auth/registry.py +256 -0
- golf/cli/__init__.py +1 -0
- golf/cli/branding.py +191 -0
- golf/cli/main.py +377 -0
- golf/commands/__init__.py +5 -0
- golf/commands/build.py +81 -0
- golf/commands/init.py +290 -0
- golf/commands/run.py +137 -0
- golf/core/__init__.py +1 -0
- golf/core/builder.py +1884 -0
- golf/core/builder_auth.py +209 -0
- golf/core/builder_metrics.py +221 -0
- golf/core/builder_telemetry.py +99 -0
- golf/core/config.py +199 -0
- golf/core/parser.py +1085 -0
- golf/core/telemetry.py +492 -0
- golf/core/transformer.py +231 -0
- golf/examples/__init__.py +0 -0
- golf/examples/basic/.env.example +4 -0
- golf/examples/basic/README.md +133 -0
- golf/examples/basic/auth.py +76 -0
- golf/examples/basic/golf.json +5 -0
- golf/examples/basic/prompts/welcome.py +27 -0
- golf/examples/basic/resources/current_time.py +34 -0
- golf/examples/basic/resources/info.py +28 -0
- golf/examples/basic/resources/weather/city.py +46 -0
- golf/examples/basic/resources/weather/client.py +48 -0
- golf/examples/basic/resources/weather/current.py +36 -0
- golf/examples/basic/resources/weather/forecast.py +36 -0
- golf/examples/basic/tools/calculator.py +94 -0
- golf/examples/basic/tools/say/hello.py +65 -0
- golf/metrics/__init__.py +10 -0
- golf/metrics/collector.py +320 -0
- golf/metrics/registry.py +12 -0
- golf/telemetry/__init__.py +23 -0
- golf/telemetry/instrumentation.py +1402 -0
- golf/utilities/__init__.py +12 -0
- golf/utilities/context.py +53 -0
- golf/utilities/elicitation.py +170 -0
- golf/utilities/sampling.py +221 -0
- golf_mcp-0.2.16.dist-info/METADATA +262 -0
- golf_mcp-0.2.16.dist-info/RECORD +52 -0
- golf_mcp-0.2.16.dist-info/WHEEL +5 -0
- golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
- golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
- 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."""
|