golf-mcp 0.1.14__py3-none-any.whl → 0.1.17__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 -1
- golf/cli/main.py +21 -12
- golf/commands/init.py +63 -1
- golf/commands/run.py +12 -4
- golf/core/builder.py +181 -61
- golf/core/config.py +13 -0
- golf/core/parser.py +585 -32
- golf/core/platform.py +180 -0
- golf/core/telemetry.py +48 -17
- golf/examples/api_key/.env.example +1 -5
- golf/examples/api_key/README.md +10 -10
- golf/examples/api_key/golf.json +1 -2
- golf/examples/basic/.env.example +3 -4
- golf/examples/basic/golf.json +1 -2
- golf/telemetry/instrumentation.py +26 -48
- {golf_mcp-0.1.14.dist-info → golf_mcp-0.1.17.dist-info}/METADATA +43 -3
- {golf_mcp-0.1.14.dist-info → golf_mcp-0.1.17.dist-info}/RECORD +21 -20
- {golf_mcp-0.1.14.dist-info → golf_mcp-0.1.17.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.14.dist-info → golf_mcp-0.1.17.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.14.dist-info → golf_mcp-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.14.dist-info → golf_mcp-0.1.17.dist-info}/top_level.txt +0 -0
golf/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.17"
|
golf/cli/main.py
CHANGED
|
@@ -121,7 +121,11 @@ def build_dev(
|
|
|
121
121
|
None, "--output-dir", "-o", help="Directory to output the built project"
|
|
122
122
|
),
|
|
123
123
|
) -> None:
|
|
124
|
-
"""Build a development version with environment variables copied.
|
|
124
|
+
"""Build a development version with app environment variables copied.
|
|
125
|
+
|
|
126
|
+
Golf credentials (GOLF_*) are always loaded from .env for build operations.
|
|
127
|
+
All environment variables are copied to the built project for development.
|
|
128
|
+
"""
|
|
125
129
|
# Find project root directory
|
|
126
130
|
project_root, config_path = find_project_root()
|
|
127
131
|
|
|
@@ -162,7 +166,7 @@ def build_dev(
|
|
|
162
166
|
e,
|
|
163
167
|
context="Development build with environment variables",
|
|
164
168
|
operation="build_dev",
|
|
165
|
-
additional_props={"environment": "dev", "copy_env": True}
|
|
169
|
+
additional_props={"environment": "dev", "copy_env": True},
|
|
166
170
|
)
|
|
167
171
|
raise
|
|
168
172
|
|
|
@@ -173,7 +177,14 @@ def build_prod(
|
|
|
173
177
|
None, "--output-dir", "-o", help="Directory to output the built project"
|
|
174
178
|
),
|
|
175
179
|
) -> None:
|
|
176
|
-
"""Build a production version
|
|
180
|
+
"""Build a production version for deployment.
|
|
181
|
+
|
|
182
|
+
Golf credentials (GOLF_*) are always loaded from .env for build operations
|
|
183
|
+
(platform registration, resource updates). App environment variables are
|
|
184
|
+
NOT copied for security - provide them in your deployment environment.
|
|
185
|
+
|
|
186
|
+
Your production deployment must include GOLF_* vars for runtime telemetry.
|
|
187
|
+
"""
|
|
177
188
|
# Find project root directory
|
|
178
189
|
project_root, config_path = find_project_root()
|
|
179
190
|
|
|
@@ -214,7 +225,7 @@ def build_prod(
|
|
|
214
225
|
e,
|
|
215
226
|
context="Production build without environment variables",
|
|
216
227
|
operation="build_prod",
|
|
217
|
-
additional_props={"environment": "prod", "copy_env": False}
|
|
228
|
+
additional_props={"environment": "prod", "copy_env": False},
|
|
218
229
|
)
|
|
219
230
|
raise
|
|
220
231
|
|
|
@@ -275,15 +286,13 @@ def run(
|
|
|
275
286
|
|
|
276
287
|
build_project(project_root, settings, dist_dir)
|
|
277
288
|
except Exception as e:
|
|
278
|
-
console.print(
|
|
279
|
-
f"[bold red]Error building project:[/bold red] {str(e)}"
|
|
280
|
-
)
|
|
289
|
+
console.print(f"[bold red]Error building project:[/bold red] {str(e)}")
|
|
281
290
|
track_detailed_error(
|
|
282
291
|
"cli_run_failed",
|
|
283
292
|
e,
|
|
284
293
|
context="Auto-build before running server",
|
|
285
294
|
operation="auto_build_before_run",
|
|
286
|
-
additional_props={"auto_build": True}
|
|
295
|
+
additional_props={"auto_build": True},
|
|
287
296
|
)
|
|
288
297
|
raise
|
|
289
298
|
else:
|
|
@@ -326,11 +335,11 @@ def run(
|
|
|
326
335
|
# 2: General interrupt/graceful shutdown
|
|
327
336
|
shutdown_type = {
|
|
328
337
|
130: "UserInterrupt",
|
|
329
|
-
143: "GracefulShutdown",
|
|
338
|
+
143: "GracefulShutdown",
|
|
330
339
|
137: "ForcedShutdown",
|
|
331
|
-
2: "Interrupt"
|
|
340
|
+
2: "Interrupt",
|
|
332
341
|
}.get(return_code, "GracefulShutdown")
|
|
333
|
-
|
|
342
|
+
|
|
334
343
|
track_event(
|
|
335
344
|
"cli_run_shutdown",
|
|
336
345
|
{
|
|
@@ -362,7 +371,7 @@ def run(
|
|
|
362
371
|
e,
|
|
363
372
|
context="Server execution or startup failure",
|
|
364
373
|
operation="run_server_execution",
|
|
365
|
-
additional_props={"has_dist_dir": dist_dir.exists() if dist_dir else False}
|
|
374
|
+
additional_props={"has_dist_dir": dist_dir.exists() if dist_dir else False},
|
|
366
375
|
)
|
|
367
376
|
raise
|
|
368
377
|
|
golf/commands/init.py
CHANGED
|
@@ -7,7 +7,12 @@ from rich.console import Console
|
|
|
7
7
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
8
|
from rich.prompt import Confirm
|
|
9
9
|
|
|
10
|
-
from golf.core.telemetry import
|
|
10
|
+
from golf.core.telemetry import (
|
|
11
|
+
track_command,
|
|
12
|
+
track_event,
|
|
13
|
+
set_telemetry_enabled,
|
|
14
|
+
load_telemetry_preference,
|
|
15
|
+
)
|
|
11
16
|
|
|
12
17
|
console = Console()
|
|
13
18
|
|
|
@@ -95,6 +100,9 @@ def initialize_project(
|
|
|
95
100
|
# Copy directory structure
|
|
96
101
|
_copy_template(template_dir, output_dir, project_name)
|
|
97
102
|
|
|
103
|
+
# Ask for telemetry consent
|
|
104
|
+
_prompt_for_telemetry_consent()
|
|
105
|
+
|
|
98
106
|
# Create virtual environment
|
|
99
107
|
console.print("[bold green]Project initialized successfully![/bold green]")
|
|
100
108
|
console.print("\nTo get started, run:")
|
|
@@ -207,6 +215,60 @@ def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> Non
|
|
|
207
215
|
f.write("dist/\n")
|
|
208
216
|
|
|
209
217
|
|
|
218
|
+
def _prompt_for_telemetry_consent() -> None:
|
|
219
|
+
"""Prompt user for telemetry consent and save their preference."""
|
|
220
|
+
import os
|
|
221
|
+
|
|
222
|
+
# Skip prompt in test mode, when telemetry is explicitly disabled, or if preference already exists
|
|
223
|
+
if os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on"):
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Skip if telemetry is explicitly disabled in environment
|
|
227
|
+
if os.environ.get("GOLF_TELEMETRY", "").lower() in ("0", "false", "no", "off"):
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Check if user already has a saved preference
|
|
231
|
+
existing_preference = load_telemetry_preference()
|
|
232
|
+
if existing_preference is not None:
|
|
233
|
+
return # User already made a choice
|
|
234
|
+
|
|
235
|
+
console.print()
|
|
236
|
+
console.rule("[bold blue]Anonymous usage analytics[/bold blue]", style="blue")
|
|
237
|
+
console.print()
|
|
238
|
+
console.print(
|
|
239
|
+
"Golf can collect [bold]anonymous usage analytics[/bold] to help improve the tool."
|
|
240
|
+
)
|
|
241
|
+
console.print()
|
|
242
|
+
console.print("[dim]What we collect:[/dim]")
|
|
243
|
+
console.print(" • Command usage (init, build, run)")
|
|
244
|
+
console.print(" • Error types (to fix bugs)")
|
|
245
|
+
console.print(" • Golf version and Python version")
|
|
246
|
+
console.print(" • Operating system type")
|
|
247
|
+
console.print()
|
|
248
|
+
console.print("[dim]What we DON'T collect:[/dim]")
|
|
249
|
+
console.print(" • Your code or project content")
|
|
250
|
+
console.print(" • File paths or project names")
|
|
251
|
+
console.print(" • Personal information")
|
|
252
|
+
console.print(" • IP addresses")
|
|
253
|
+
console.print()
|
|
254
|
+
console.print(
|
|
255
|
+
"You can change this anytime by setting GOLF_TELEMETRY=0 in your environment."
|
|
256
|
+
)
|
|
257
|
+
console.print()
|
|
258
|
+
|
|
259
|
+
enable_telemetry = Confirm.ask(
|
|
260
|
+
"[bold]Enable anonymous usage analytics?[/bold]", default=False
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
set_telemetry_enabled(enable_telemetry, persist=True)
|
|
264
|
+
|
|
265
|
+
if enable_telemetry:
|
|
266
|
+
console.print("[green]✓[/green] Anonymous analytics enabled")
|
|
267
|
+
else:
|
|
268
|
+
console.print("[yellow]○[/yellow] Anonymous analytics disabled")
|
|
269
|
+
console.print()
|
|
270
|
+
|
|
271
|
+
|
|
210
272
|
def _is_text_file(path: Path) -> bool:
|
|
211
273
|
"""Check if a file is a text file that needs variable substitution.
|
|
212
274
|
|
golf/commands/run.py
CHANGED
|
@@ -70,13 +70,21 @@ def run_server(
|
|
|
70
70
|
elif process.returncode == 130:
|
|
71
71
|
console.print("[yellow]Server stopped by user interrupt (Ctrl+C)[/yellow]")
|
|
72
72
|
elif process.returncode == 143:
|
|
73
|
-
console.print(
|
|
73
|
+
console.print(
|
|
74
|
+
"[yellow]Server stopped by SIGTERM (graceful shutdown)[/yellow]"
|
|
75
|
+
)
|
|
74
76
|
elif process.returncode == 137:
|
|
75
|
-
console.print(
|
|
77
|
+
console.print(
|
|
78
|
+
"[yellow]Server stopped by SIGKILL (forced shutdown)[/yellow]"
|
|
79
|
+
)
|
|
76
80
|
elif process.returncode in [1, 2]:
|
|
77
|
-
console.print(
|
|
81
|
+
console.print(
|
|
82
|
+
f"[red]Server exited with error code {process.returncode}[/red]"
|
|
83
|
+
)
|
|
78
84
|
else:
|
|
79
|
-
console.print(
|
|
85
|
+
console.print(
|
|
86
|
+
f"[orange]Server exited with code {process.returncode}[/orange]"
|
|
87
|
+
)
|
|
80
88
|
|
|
81
89
|
return process.returncode
|
|
82
90
|
except KeyboardInterrupt:
|
golf/core/builder.py
CHANGED
|
@@ -97,6 +97,11 @@ class ManifestBuilder:
|
|
|
97
97
|
if required_fields:
|
|
98
98
|
tool_schema["inputSchema"]["required"] = required_fields
|
|
99
99
|
|
|
100
|
+
# Add tool annotations if present
|
|
101
|
+
if component.annotations:
|
|
102
|
+
# Merge with existing annotations (keeping title)
|
|
103
|
+
tool_schema["annotations"].update(component.annotations)
|
|
104
|
+
|
|
100
105
|
# Add the tool to the manifest
|
|
101
106
|
self.manifest["tools"].append(tool_schema)
|
|
102
107
|
|
|
@@ -556,14 +561,15 @@ class CodeGenerator:
|
|
|
556
561
|
# Add OpenTelemetry imports if enabled
|
|
557
562
|
if self.settings.opentelemetry_enabled:
|
|
558
563
|
imports.extend(generate_telemetry_imports())
|
|
559
|
-
imports.append("")
|
|
560
564
|
|
|
561
|
-
# Add
|
|
562
|
-
if self.settings.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
565
|
+
# Add health check imports if enabled
|
|
566
|
+
if self.settings.health_check_enabled:
|
|
567
|
+
imports.extend(
|
|
568
|
+
[
|
|
569
|
+
"from starlette.requests import Request",
|
|
570
|
+
"from starlette.responses import PlainTextResponse",
|
|
571
|
+
]
|
|
572
|
+
)
|
|
567
573
|
|
|
568
574
|
# Get transport-specific configuration
|
|
569
575
|
transport_config = self._get_transport_config(self.settings.transport)
|
|
@@ -650,7 +656,10 @@ class CodeGenerator:
|
|
|
650
656
|
# Add code to register this component
|
|
651
657
|
if self.settings.opentelemetry_enabled:
|
|
652
658
|
# Use telemetry instrumentation
|
|
653
|
-
registration =
|
|
659
|
+
registration = (
|
|
660
|
+
f"# Register the {component_type.value} "
|
|
661
|
+
f"'{component.name}' with telemetry"
|
|
662
|
+
)
|
|
654
663
|
entry_func = (
|
|
655
664
|
component.entry_function
|
|
656
665
|
if hasattr(component, "entry_function")
|
|
@@ -658,15 +667,31 @@ class CodeGenerator:
|
|
|
658
667
|
else "export"
|
|
659
668
|
)
|
|
660
669
|
|
|
661
|
-
|
|
662
|
-
|
|
670
|
+
registration += (
|
|
671
|
+
f"\n_wrapped_func = instrument_{component_type.value}("
|
|
672
|
+
f"{full_module_path}.{entry_func}, '{component.name}')"
|
|
673
|
+
)
|
|
663
674
|
|
|
664
675
|
if component_type == ComponentType.TOOL:
|
|
665
|
-
registration +=
|
|
676
|
+
registration += (
|
|
677
|
+
f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
|
|
678
|
+
f'description="{component.docstring or ""}"'
|
|
679
|
+
)
|
|
680
|
+
# Add annotations if present
|
|
681
|
+
if hasattr(component, "annotations") and component.annotations:
|
|
682
|
+
registration += f", annotations={component.annotations}"
|
|
683
|
+
registration += ")"
|
|
666
684
|
elif component_type == ComponentType.RESOURCE:
|
|
667
|
-
registration +=
|
|
685
|
+
registration += (
|
|
686
|
+
f"\nmcp.add_resource_fn(_wrapped_func, "
|
|
687
|
+
f'uri="{component.uri_template}", name="{component.name}", '
|
|
688
|
+
f'description="{component.docstring or ""}")'
|
|
689
|
+
)
|
|
668
690
|
else: # PROMPT
|
|
669
|
-
registration +=
|
|
691
|
+
registration += (
|
|
692
|
+
f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
|
|
693
|
+
f'description="{component.docstring or ""}")'
|
|
694
|
+
)
|
|
670
695
|
else:
|
|
671
696
|
# Standard registration without telemetry
|
|
672
697
|
if component_type == ComponentType.TOOL:
|
|
@@ -689,6 +714,11 @@ class CodeGenerator:
|
|
|
689
714
|
# Escape any quotes in the docstring
|
|
690
715
|
escaped_docstring = component.docstring.replace('"', '\\"')
|
|
691
716
|
registration += f', description="{escaped_docstring}"'
|
|
717
|
+
|
|
718
|
+
# Add annotations if present
|
|
719
|
+
if hasattr(component, "annotations") and component.annotations:
|
|
720
|
+
registration += f", annotations={component.annotations}"
|
|
721
|
+
|
|
692
722
|
registration += ")"
|
|
693
723
|
|
|
694
724
|
elif component_type == ComponentType.RESOURCE:
|
|
@@ -711,6 +741,7 @@ class CodeGenerator:
|
|
|
711
741
|
# Escape any quotes in the docstring
|
|
712
742
|
escaped_docstring = component.docstring.replace('"', '\\"')
|
|
713
743
|
registration += f', description="{escaped_docstring}"'
|
|
744
|
+
|
|
714
745
|
registration += ")"
|
|
715
746
|
|
|
716
747
|
else: # PROMPT
|
|
@@ -735,6 +766,7 @@ class CodeGenerator:
|
|
|
735
766
|
# Escape any quotes in the docstring
|
|
736
767
|
escaped_docstring = component.docstring.replace('"', '\\"')
|
|
737
768
|
registration += f', description="{escaped_docstring}"'
|
|
769
|
+
|
|
738
770
|
registration += ")"
|
|
739
771
|
|
|
740
772
|
component_registrations.append(registration)
|
|
@@ -770,6 +802,10 @@ class CodeGenerator:
|
|
|
770
802
|
for key, value in auth_components["fastmcp_args"].items():
|
|
771
803
|
mcp_constructor_args.append(f"{key}={value}")
|
|
772
804
|
|
|
805
|
+
# Add stateless HTTP parameter if enabled
|
|
806
|
+
if self.settings.stateless_http:
|
|
807
|
+
mcp_constructor_args.append("stateless_http=True")
|
|
808
|
+
|
|
773
809
|
# Add OpenTelemetry parameters if enabled
|
|
774
810
|
if self.settings.opentelemetry_enabled:
|
|
775
811
|
mcp_constructor_args.append("lifespan=telemetry_lifespan")
|
|
@@ -778,6 +814,18 @@ class CodeGenerator:
|
|
|
778
814
|
server_code_lines.append(mcp_instance_line)
|
|
779
815
|
server_code_lines.append("")
|
|
780
816
|
|
|
817
|
+
# Add early telemetry initialization if enabled (before component registration)
|
|
818
|
+
early_telemetry_init = []
|
|
819
|
+
if self.settings.opentelemetry_enabled:
|
|
820
|
+
early_telemetry_init.extend(
|
|
821
|
+
[
|
|
822
|
+
"# Initialize telemetry early to ensure instrumentation works",
|
|
823
|
+
"from golf.telemetry.instrumentation import init_telemetry",
|
|
824
|
+
f'init_telemetry("{self.settings.name}")',
|
|
825
|
+
"",
|
|
826
|
+
]
|
|
827
|
+
)
|
|
828
|
+
|
|
781
829
|
# Main entry point with transport-specific app initialization
|
|
782
830
|
main_code = [
|
|
783
831
|
'if __name__ == "__main__":',
|
|
@@ -805,86 +853,115 @@ class CodeGenerator:
|
|
|
805
853
|
|
|
806
854
|
# Transport-specific run methods
|
|
807
855
|
if self.settings.transport == "sse":
|
|
808
|
-
# Check if we need
|
|
856
|
+
# Check if we need middleware for SSE
|
|
857
|
+
middleware_setup = []
|
|
858
|
+
middleware_list = []
|
|
859
|
+
|
|
809
860
|
api_key_config = get_api_key_config()
|
|
810
861
|
if auth_components.get("has_auth") and api_key_config:
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
" # For SSE with API key auth, we need to get the app and add middleware",
|
|
814
|
-
' app = mcp.http_app(transport="sse")',
|
|
815
|
-
" app.add_middleware(ApiKeyMiddleware)",
|
|
816
|
-
]
|
|
862
|
+
middleware_setup.append(
|
|
863
|
+
" from starlette.middleware import Middleware"
|
|
817
864
|
)
|
|
818
|
-
|
|
865
|
+
middleware_list.append("Middleware(ApiKeyMiddleware)")
|
|
866
|
+
|
|
867
|
+
# Add OpenTelemetry middleware if enabled
|
|
868
|
+
if self.settings.opentelemetry_enabled:
|
|
869
|
+
middleware_setup.append(
|
|
870
|
+
" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
|
|
871
|
+
)
|
|
872
|
+
middleware_setup.append(
|
|
873
|
+
" from starlette.middleware import Middleware"
|
|
874
|
+
)
|
|
875
|
+
middleware_list.append("Middleware(OpenTelemetryMiddleware)")
|
|
876
|
+
|
|
877
|
+
if middleware_setup:
|
|
878
|
+
main_code.extend(middleware_setup)
|
|
879
|
+
main_code.append(f" middleware = [{', '.join(middleware_list)}]")
|
|
880
|
+
main_code.append("")
|
|
819
881
|
main_code.extend(
|
|
820
882
|
[
|
|
821
|
-
" #
|
|
822
|
-
'
|
|
883
|
+
" # Run SSE server with middleware using FastMCP's run method",
|
|
884
|
+
' mcp.run(transport="sse", host=host, port=port, log_level="info", middleware=middleware)',
|
|
823
885
|
]
|
|
824
886
|
)
|
|
825
|
-
|
|
826
|
-
# Add OpenTelemetry middleware to the SSE app if enabled
|
|
827
|
-
if self.settings.opentelemetry_enabled:
|
|
887
|
+
else:
|
|
828
888
|
main_code.extend(
|
|
829
889
|
[
|
|
830
|
-
" #
|
|
831
|
-
|
|
832
|
-
" app = OpenTelemetryMiddleware(app)",
|
|
890
|
+
" # Run SSE server using FastMCP's run method",
|
|
891
|
+
' mcp.run(transport="sse", host=host, port=port, log_level="info")',
|
|
833
892
|
]
|
|
834
893
|
)
|
|
835
894
|
|
|
836
|
-
main_code.extend(
|
|
837
|
-
[
|
|
838
|
-
" # Run with the configured app",
|
|
839
|
-
' uvicorn.run(app, host=host, port=port, log_level="info")',
|
|
840
|
-
]
|
|
841
|
-
)
|
|
842
895
|
elif self.settings.transport in ["streamable-http", "http"]:
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
" app = mcp.http_app()",
|
|
847
|
-
]
|
|
848
|
-
)
|
|
896
|
+
# Check if we need middleware for streamable-http
|
|
897
|
+
middleware_setup = []
|
|
898
|
+
middleware_list = []
|
|
849
899
|
|
|
850
|
-
# Check if we need to add API key middleware
|
|
851
900
|
api_key_config = get_api_key_config()
|
|
852
901
|
if auth_components.get("has_auth") and api_key_config:
|
|
902
|
+
middleware_setup.append(
|
|
903
|
+
" from starlette.middleware import Middleware"
|
|
904
|
+
)
|
|
905
|
+
middleware_list.append("Middleware(ApiKeyMiddleware)")
|
|
906
|
+
|
|
907
|
+
# Add OpenTelemetry middleware if enabled
|
|
908
|
+
if self.settings.opentelemetry_enabled:
|
|
909
|
+
middleware_setup.append(
|
|
910
|
+
" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
|
|
911
|
+
)
|
|
912
|
+
middleware_setup.append(
|
|
913
|
+
" from starlette.middleware import Middleware"
|
|
914
|
+
)
|
|
915
|
+
middleware_list.append("Middleware(OpenTelemetryMiddleware)")
|
|
916
|
+
|
|
917
|
+
if middleware_setup:
|
|
918
|
+
main_code.extend(middleware_setup)
|
|
919
|
+
main_code.append(f" middleware = [{', '.join(middleware_list)}]")
|
|
920
|
+
main_code.append("")
|
|
853
921
|
main_code.extend(
|
|
854
922
|
[
|
|
855
|
-
" #
|
|
856
|
-
|
|
923
|
+
" # Run HTTP server with middleware using FastMCP's run method",
|
|
924
|
+
' mcp.run(transport="streamable-http", host=host, port=port, log_level="info", middleware=middleware)',
|
|
857
925
|
]
|
|
858
926
|
)
|
|
859
|
-
|
|
860
|
-
# Add OpenTelemetry middleware to the HTTP app if enabled
|
|
861
|
-
if self.settings.opentelemetry_enabled:
|
|
927
|
+
else:
|
|
862
928
|
main_code.extend(
|
|
863
929
|
[
|
|
864
|
-
" #
|
|
865
|
-
|
|
866
|
-
" app = OpenTelemetryMiddleware(app)",
|
|
930
|
+
" # Run HTTP server using FastMCP's run method",
|
|
931
|
+
' mcp.run(transport="streamable-http", host=host, port=port, log_level="info")',
|
|
867
932
|
]
|
|
868
933
|
)
|
|
869
|
-
|
|
870
|
-
main_code.extend(
|
|
871
|
-
[' uvicorn.run(app, host=host, port=port, log_level="info")']
|
|
872
|
-
)
|
|
873
934
|
else:
|
|
874
935
|
# For stdio transport, use mcp.run()
|
|
875
936
|
main_code.extend(
|
|
876
937
|
[" # Run with stdio transport", ' mcp.run(transport="stdio")']
|
|
877
938
|
)
|
|
878
939
|
|
|
940
|
+
# Add health check route if enabled
|
|
941
|
+
health_check_code = []
|
|
942
|
+
if self.settings.health_check_enabled:
|
|
943
|
+
health_check_code = [
|
|
944
|
+
"# Add health check route",
|
|
945
|
+
"@mcp.custom_route('"
|
|
946
|
+
+ self.settings.health_check_path
|
|
947
|
+
+ '\', methods=["GET"])',
|
|
948
|
+
"async def health_check(request: Request) -> PlainTextResponse:",
|
|
949
|
+
' """Health check endpoint for Kubernetes and load balancers."""',
|
|
950
|
+
f' return PlainTextResponse("{self.settings.health_check_response}")',
|
|
951
|
+
"",
|
|
952
|
+
]
|
|
953
|
+
|
|
879
954
|
# Combine all sections
|
|
880
955
|
# Order: imports, env_section, auth_setup, server_code (mcp init),
|
|
881
|
-
#
|
|
956
|
+
# early_telemetry_init, component_registrations, health_check_code, main_code (run block)
|
|
882
957
|
code = "\n".join(
|
|
883
958
|
imports
|
|
884
959
|
+ env_section
|
|
885
960
|
+ auth_setup_code
|
|
886
961
|
+ server_code_lines
|
|
962
|
+
+ early_telemetry_init
|
|
887
963
|
+ component_registrations
|
|
964
|
+
+ health_check_code
|
|
888
965
|
+ main_code
|
|
889
966
|
)
|
|
890
967
|
|
|
@@ -915,6 +992,21 @@ def build_project(
|
|
|
915
992
|
build_env: Build environment ('dev' or 'prod')
|
|
916
993
|
copy_env: Whether to copy environment variables to the built app
|
|
917
994
|
"""
|
|
995
|
+
# Load Golf credentials from .env for build operations (platform registration, etc.)
|
|
996
|
+
# This happens regardless of copy_env setting to ensure build process works
|
|
997
|
+
from dotenv import load_dotenv
|
|
998
|
+
|
|
999
|
+
project_env_file = project_path / ".env"
|
|
1000
|
+
if project_env_file.exists():
|
|
1001
|
+
# Load GOLF_* variables for build process
|
|
1002
|
+
load_dotenv(project_env_file, override=False)
|
|
1003
|
+
|
|
1004
|
+
# Only log if we actually found the specific Golf platform credentials
|
|
1005
|
+
has_api_key = "GOLF_API_KEY" in os.environ
|
|
1006
|
+
has_server_id = "GOLF_SERVER_ID" in os.environ
|
|
1007
|
+
if has_api_key and has_server_id:
|
|
1008
|
+
console.print("[dim]Loaded Golf credentials for build operations[/dim]")
|
|
1009
|
+
|
|
918
1010
|
# Execute pre_build.py if it exists
|
|
919
1011
|
pre_build_path = project_path / "pre_build.py"
|
|
920
1012
|
if pre_build_path.exists():
|
|
@@ -950,10 +1042,11 @@ def build_project(
|
|
|
950
1042
|
import traceback
|
|
951
1043
|
|
|
952
1044
|
console.print(f"[red]{traceback.format_exc()}[/red]")
|
|
953
|
-
|
|
1045
|
+
|
|
954
1046
|
# Track detailed error for pre_build.py execution failures
|
|
955
1047
|
try:
|
|
956
1048
|
from golf.core.telemetry import track_detailed_error
|
|
1049
|
+
|
|
957
1050
|
track_detailed_error(
|
|
958
1051
|
"build_pre_build_failed",
|
|
959
1052
|
e,
|
|
@@ -962,7 +1055,7 @@ def build_project(
|
|
|
962
1055
|
additional_props={
|
|
963
1056
|
"file_path": str(pre_build_path.relative_to(project_path)),
|
|
964
1057
|
"build_env": build_env,
|
|
965
|
-
}
|
|
1058
|
+
},
|
|
966
1059
|
)
|
|
967
1060
|
except Exception:
|
|
968
1061
|
# Don't let telemetry errors break the build
|
|
@@ -1015,9 +1108,6 @@ def build_project(
|
|
|
1015
1108
|
env_vars_to_write["OTEL_TRACES_EXPORTER"] = (
|
|
1016
1109
|
settings.opentelemetry_default_exporter
|
|
1017
1110
|
)
|
|
1018
|
-
console.print(
|
|
1019
|
-
f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]"
|
|
1020
|
-
)
|
|
1021
1111
|
|
|
1022
1112
|
# 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
|
|
1023
1113
|
# (Ensures service name defaults to project name if not specified in user's .env)
|
|
@@ -1069,6 +1159,36 @@ def build_project(
|
|
|
1069
1159
|
)
|
|
1070
1160
|
generator.generate()
|
|
1071
1161
|
|
|
1162
|
+
# Platform registration (only for prod builds)
|
|
1163
|
+
if build_env == "prod":
|
|
1164
|
+
console.print(
|
|
1165
|
+
"[dim]Registering with Golf platform and updating resources...[/dim]"
|
|
1166
|
+
)
|
|
1167
|
+
import asyncio
|
|
1168
|
+
|
|
1169
|
+
try:
|
|
1170
|
+
from golf.core.platform import register_project_with_platform
|
|
1171
|
+
|
|
1172
|
+
asyncio.run(
|
|
1173
|
+
register_project_with_platform(
|
|
1174
|
+
project_path=project_path,
|
|
1175
|
+
settings=settings,
|
|
1176
|
+
components=generator.components,
|
|
1177
|
+
)
|
|
1178
|
+
)
|
|
1179
|
+
console.print("[green]✓ Platform registration completed[/green]")
|
|
1180
|
+
except ImportError:
|
|
1181
|
+
console.print(
|
|
1182
|
+
"[yellow]Warning: Platform registration module not available[/yellow]"
|
|
1183
|
+
)
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
console.print(
|
|
1186
|
+
f"[yellow]Warning: Platform registration failed: {e}[/yellow]"
|
|
1187
|
+
)
|
|
1188
|
+
console.print(
|
|
1189
|
+
"[yellow]Tip: Ensure GOLF_API_KEY and GOLF_SERVER_ID are available in your .env file[/yellow]"
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1072
1192
|
# Create a simple README
|
|
1073
1193
|
readme_content = f"""# {settings.name}
|
|
1074
1194
|
|
|
@@ -1089,7 +1209,7 @@ This is a standalone FastMCP server generated by GolfMCP.
|
|
|
1089
1209
|
|
|
1090
1210
|
# Copy pyproject.toml with required dependencies
|
|
1091
1211
|
base_dependencies = [
|
|
1092
|
-
"fastmcp>=2.0.0",
|
|
1212
|
+
"fastmcp>=2.0.0,<2.6.0",
|
|
1093
1213
|
"uvicorn>=0.20.0",
|
|
1094
1214
|
"pydantic>=2.0.0",
|
|
1095
1215
|
"python-dotenv>=1.0.0",
|
golf/core/config.py
CHANGED
|
@@ -102,6 +102,19 @@ class Settings(BaseSettings):
|
|
|
102
102
|
"console", description="Default OpenTelemetry exporter type"
|
|
103
103
|
)
|
|
104
104
|
|
|
105
|
+
# Health check configuration
|
|
106
|
+
health_check_enabled: bool = Field(
|
|
107
|
+
False, description="Enable health check endpoint"
|
|
108
|
+
)
|
|
109
|
+
health_check_path: str = Field("/health", description="Health check endpoint path")
|
|
110
|
+
health_check_response: str = Field("OK", description="Health check response text")
|
|
111
|
+
|
|
112
|
+
# HTTP session behaviour
|
|
113
|
+
stateless_http: bool = Field(
|
|
114
|
+
False,
|
|
115
|
+
description="Make Streamable-HTTP transport stateless (new session per request)",
|
|
116
|
+
)
|
|
117
|
+
|
|
105
118
|
|
|
106
119
|
def find_config_path(start_path: Path | None = None) -> Path | None:
|
|
107
120
|
"""Find the golf config file by searching upwards from the given path.
|