golf-mcp 0.1.16__py3-none-any.whl → 0.1.18__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.16"
1
+ __version__ = "0.1.18"
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
 
@@ -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 without copying environment variables."""
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
 
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 track_command, track_event
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/core/builder.py CHANGED
@@ -561,14 +561,18 @@ class CodeGenerator:
561
561
  # Add OpenTelemetry imports if enabled
562
562
  if self.settings.opentelemetry_enabled:
563
563
  imports.extend(generate_telemetry_imports())
564
- imports.append("")
565
564
 
566
- # Add imports section for different transport methods
567
- if self.settings.transport == "sse" or self.settings.transport in [
568
- "streamable-http",
569
- "http",
570
- ]:
571
- imports.append("import uvicorn")
565
+ # Add metrics imports if enabled
566
+ if self.settings.metrics_enabled:
567
+ from golf.core.builder_metrics import (
568
+ generate_metrics_imports,
569
+ generate_metrics_instrumentation,
570
+ generate_session_tracking,
571
+ )
572
+
573
+ imports.extend(generate_metrics_imports())
574
+ imports.extend(generate_metrics_instrumentation())
575
+ imports.extend(generate_session_tracking())
572
576
 
573
577
  # Add health check imports if enabled
574
578
  if self.settings.health_check_enabled:
@@ -664,7 +668,48 @@ class CodeGenerator:
664
668
  # Add code to register this component
665
669
  if self.settings.opentelemetry_enabled:
666
670
  # Use telemetry instrumentation
667
- registration = f"# Register the {component_type.value} '{component.name}' with telemetry"
671
+ registration = (
672
+ f"# Register the {component_type.value} "
673
+ f"'{component.name}' with telemetry"
674
+ )
675
+ entry_func = (
676
+ component.entry_function
677
+ if hasattr(component, "entry_function")
678
+ and component.entry_function
679
+ else "export"
680
+ )
681
+
682
+ registration += (
683
+ f"\n_wrapped_func = instrument_{component_type.value}("
684
+ f"{full_module_path}.{entry_func}, '{component.name}')"
685
+ )
686
+
687
+ if component_type == ComponentType.TOOL:
688
+ registration += (
689
+ f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
690
+ f'description="{component.docstring or ""}"'
691
+ )
692
+ # Add annotations if present
693
+ if hasattr(component, "annotations") and component.annotations:
694
+ registration += f", annotations={component.annotations}"
695
+ registration += ")"
696
+ elif component_type == ComponentType.RESOURCE:
697
+ registration += (
698
+ f"\nmcp.add_resource_fn(_wrapped_func, "
699
+ f'uri="{component.uri_template}", name="{component.name}", '
700
+ f'description="{component.docstring or ""}")'
701
+ )
702
+ else: # PROMPT
703
+ registration += (
704
+ f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
705
+ f'description="{component.docstring or ""}")'
706
+ )
707
+ elif self.settings.metrics_enabled:
708
+ # Use metrics instrumentation
709
+ registration = (
710
+ f"# Register the {component_type.value} "
711
+ f"'{component.name}' with metrics"
712
+ )
668
713
  entry_func = (
669
714
  component.entry_function
670
715
  if hasattr(component, "entry_function")
@@ -672,19 +717,31 @@ class CodeGenerator:
672
717
  else "export"
673
718
  )
674
719
 
675
- # Debug: Add logging to verify wrapping
676
- registration += f"\n_wrapped_func = instrument_{component_type.value}({full_module_path}.{entry_func}, '{component.name}')"
720
+ registration += (
721
+ f"\n_wrapped_func = instrument_{component_type.value}("
722
+ f"{full_module_path}.{entry_func}, '{component.name}')"
723
+ )
677
724
 
678
725
  if component_type == ComponentType.TOOL:
679
- registration += f'\nmcp.add_tool(_wrapped_func, name="{component.name}", description="{component.docstring or ""}"'
726
+ registration += (
727
+ f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
728
+ f'description="{component.docstring or ""}"'
729
+ )
680
730
  # Add annotations if present
681
731
  if hasattr(component, "annotations") and component.annotations:
682
732
  registration += f", annotations={component.annotations}"
683
733
  registration += ")"
684
734
  elif component_type == ComponentType.RESOURCE:
685
- registration += f'\nmcp.add_resource_fn(_wrapped_func, uri="{component.uri_template}", name="{component.name}", description="{component.docstring or ""}")'
735
+ registration += (
736
+ f"\nmcp.add_resource_fn(_wrapped_func, "
737
+ f'uri="{component.uri_template}", name="{component.name}", '
738
+ f'description="{component.docstring or ""}")'
739
+ )
686
740
  else: # PROMPT
687
- registration += f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", description="{component.docstring or ""}")'
741
+ registration += (
742
+ f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
743
+ f'description="{component.docstring or ""}")'
744
+ )
688
745
  else:
689
746
  # Standard registration without telemetry
690
747
  if component_type == ComponentType.TOOL:
@@ -795,6 +852,10 @@ class CodeGenerator:
795
852
  for key, value in auth_components["fastmcp_args"].items():
796
853
  mcp_constructor_args.append(f"{key}={value}")
797
854
 
855
+ # Add stateless HTTP parameter if enabled
856
+ if self.settings.stateless_http:
857
+ mcp_constructor_args.append("stateless_http=True")
858
+
798
859
  # Add OpenTelemetry parameters if enabled
799
860
  if self.settings.opentelemetry_enabled:
800
861
  mcp_constructor_args.append("lifespan=telemetry_lifespan")
@@ -803,6 +864,27 @@ class CodeGenerator:
803
864
  server_code_lines.append(mcp_instance_line)
804
865
  server_code_lines.append("")
805
866
 
867
+ # Add early telemetry initialization if enabled (before component registration)
868
+ early_telemetry_init = []
869
+ if self.settings.opentelemetry_enabled:
870
+ early_telemetry_init.extend(
871
+ [
872
+ "# Initialize telemetry early to ensure instrumentation works",
873
+ "from golf.telemetry.instrumentation import init_telemetry",
874
+ f'init_telemetry("{self.settings.name}")',
875
+ "",
876
+ ]
877
+ )
878
+
879
+ # Add metrics initialization if enabled
880
+ early_metrics_init = []
881
+ if self.settings.metrics_enabled:
882
+ from golf.core.builder_metrics import generate_metrics_initialization
883
+
884
+ early_metrics_init.extend(
885
+ generate_metrics_initialization(self.settings.name)
886
+ )
887
+
806
888
  # Main entry point with transport-specific app initialization
807
889
  main_code = [
808
890
  'if __name__ == "__main__":',
@@ -830,77 +912,111 @@ class CodeGenerator:
830
912
 
831
913
  # Transport-specific run methods
832
914
  if self.settings.transport == "sse":
833
- # Check if we need to add API key middleware for SSE
915
+ # Check if we need middleware for SSE
916
+ middleware_setup = []
917
+ middleware_list = []
918
+
834
919
  api_key_config = get_api_key_config()
835
920
  if auth_components.get("has_auth") and api_key_config:
836
- main_code.extend(
837
- [
838
- " # For SSE with API key auth, we need to get the app and add middleware",
839
- ' app = mcp.http_app(transport="sse")',
840
- " app.add_middleware(ApiKeyMiddleware)",
841
- ]
921
+ middleware_setup.append(
922
+ " from starlette.middleware import Middleware"
842
923
  )
843
- else:
924
+ middleware_list.append("Middleware(ApiKeyMiddleware)")
925
+
926
+ # Add metrics middleware if enabled
927
+ if self.settings.metrics_enabled:
928
+ middleware_setup.append(
929
+ " from starlette.middleware import Middleware"
930
+ )
931
+ middleware_list.append("Middleware(MetricsMiddleware)")
932
+
933
+ # Add OpenTelemetry middleware if enabled
934
+ if self.settings.opentelemetry_enabled:
935
+ middleware_setup.append(
936
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
937
+ )
938
+ middleware_setup.append(
939
+ " from starlette.middleware import Middleware"
940
+ )
941
+ middleware_list.append("Middleware(OpenTelemetryMiddleware)")
942
+
943
+ if middleware_setup:
944
+ main_code.extend(middleware_setup)
945
+ main_code.append(f" middleware = [{', '.join(middleware_list)}]")
946
+ main_code.append("")
844
947
  main_code.extend(
845
948
  [
846
- " # For SSE, get the app to add middleware",
847
- ' app = mcp.http_app(transport="sse")',
949
+ " # Run SSE server with middleware using FastMCP's run method",
950
+ ' mcp.run(transport="sse", host=host, port=port, log_level="info", middleware=middleware)',
848
951
  ]
849
952
  )
850
-
851
- # Add OpenTelemetry middleware to the SSE app if enabled
852
- if self.settings.opentelemetry_enabled:
953
+ else:
853
954
  main_code.extend(
854
955
  [
855
- " # Apply OpenTelemetry middleware to the SSE app",
856
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
857
- " app = OpenTelemetryMiddleware(app)",
956
+ " # Run SSE server using FastMCP's run method",
957
+ ' mcp.run(transport="sse", host=host, port=port, log_level="info")',
858
958
  ]
859
959
  )
860
960
 
861
- main_code.extend(
862
- [
863
- " # Run with the configured app",
864
- ' uvicorn.run(app, host=host, port=port, log_level="info")',
865
- ]
866
- )
867
961
  elif self.settings.transport in ["streamable-http", "http"]:
868
- main_code.extend(
869
- [
870
- " # Create HTTP app and run with uvicorn",
871
- " app = mcp.http_app()",
872
- ]
873
- )
962
+ # Check if we need middleware for streamable-http
963
+ middleware_setup = []
964
+ middleware_list = []
874
965
 
875
- # Check if we need to add API key middleware
876
966
  api_key_config = get_api_key_config()
877
967
  if auth_components.get("has_auth") and api_key_config:
968
+ middleware_setup.append(
969
+ " from starlette.middleware import Middleware"
970
+ )
971
+ middleware_list.append("Middleware(ApiKeyMiddleware)")
972
+
973
+ # Add metrics middleware if enabled
974
+ if self.settings.metrics_enabled:
975
+ middleware_setup.append(
976
+ " from starlette.middleware import Middleware"
977
+ )
978
+ middleware_list.append("Middleware(MetricsMiddleware)")
979
+
980
+ # Add OpenTelemetry middleware if enabled
981
+ if self.settings.opentelemetry_enabled:
982
+ middleware_setup.append(
983
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
984
+ )
985
+ middleware_setup.append(
986
+ " from starlette.middleware import Middleware"
987
+ )
988
+ middleware_list.append("Middleware(OpenTelemetryMiddleware)")
989
+
990
+ if middleware_setup:
991
+ main_code.extend(middleware_setup)
992
+ main_code.append(f" middleware = [{', '.join(middleware_list)}]")
993
+ main_code.append("")
878
994
  main_code.extend(
879
995
  [
880
- " # Add API key middleware",
881
- " app.add_middleware(ApiKeyMiddleware)",
996
+ " # Run HTTP server with middleware using FastMCP's run method",
997
+ ' mcp.run(transport="streamable-http", host=host, port=port, log_level="info", middleware=middleware)',
882
998
  ]
883
999
  )
884
-
885
- # Add OpenTelemetry middleware to the HTTP app if enabled
886
- if self.settings.opentelemetry_enabled:
1000
+ else:
887
1001
  main_code.extend(
888
1002
  [
889
- " # Apply OpenTelemetry middleware to the HTTP app",
890
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
891
- " app = OpenTelemetryMiddleware(app)",
1003
+ " # Run HTTP server using FastMCP's run method",
1004
+ ' mcp.run(transport="streamable-http", host=host, port=port, log_level="info")',
892
1005
  ]
893
1006
  )
894
-
895
- main_code.extend(
896
- [' uvicorn.run(app, host=host, port=port, log_level="info")']
897
- )
898
1007
  else:
899
1008
  # For stdio transport, use mcp.run()
900
1009
  main_code.extend(
901
1010
  [" # Run with stdio transport", ' mcp.run(transport="stdio")']
902
1011
  )
903
1012
 
1013
+ # Add metrics route if enabled
1014
+ metrics_route_code = []
1015
+ if self.settings.metrics_enabled:
1016
+ from golf.core.builder_metrics import generate_metrics_route
1017
+
1018
+ metrics_route_code = generate_metrics_route(self.settings.metrics_path)
1019
+
904
1020
  # Add health check route if enabled
905
1021
  health_check_code = []
906
1022
  if self.settings.health_check_enabled:
@@ -917,13 +1033,16 @@ class CodeGenerator:
917
1033
 
918
1034
  # Combine all sections
919
1035
  # Order: imports, env_section, auth_setup, server_code (mcp init),
920
- # post_init (API key middleware), component_registrations, main_code (run block)
1036
+ # early_telemetry_init, early_metrics_init, component_registrations, metrics_route_code, health_check_code, main_code (run block)
921
1037
  code = "\n".join(
922
1038
  imports
923
1039
  + env_section
924
1040
  + auth_setup_code
925
1041
  + server_code_lines
1042
+ + early_telemetry_init
1043
+ + early_metrics_init
926
1044
  + component_registrations
1045
+ + metrics_route_code
927
1046
  + health_check_code
928
1047
  + main_code
929
1048
  )
@@ -955,6 +1074,21 @@ def build_project(
955
1074
  build_env: Build environment ('dev' or 'prod')
956
1075
  copy_env: Whether to copy environment variables to the built app
957
1076
  """
1077
+ # Load Golf credentials from .env for build operations (platform registration, etc.)
1078
+ # This happens regardless of copy_env setting to ensure build process works
1079
+ from dotenv import load_dotenv
1080
+
1081
+ project_env_file = project_path / ".env"
1082
+ if project_env_file.exists():
1083
+ # Load GOLF_* variables for build process
1084
+ load_dotenv(project_env_file, override=False)
1085
+
1086
+ # Only log if we actually found the specific Golf platform credentials
1087
+ has_api_key = "GOLF_API_KEY" in os.environ
1088
+ has_server_id = "GOLF_SERVER_ID" in os.environ
1089
+ if has_api_key and has_server_id:
1090
+ console.print("[dim]Loaded Golf credentials for build operations[/dim]")
1091
+
958
1092
  # Execute pre_build.py if it exists
959
1093
  pre_build_path = project_path / "pre_build.py"
960
1094
  if pre_build_path.exists():
@@ -1056,9 +1190,6 @@ def build_project(
1056
1190
  env_vars_to_write["OTEL_TRACES_EXPORTER"] = (
1057
1191
  settings.opentelemetry_default_exporter
1058
1192
  )
1059
- console.print(
1060
- f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]"
1061
- )
1062
1193
 
1063
1194
  # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
1064
1195
  # (Ensures service name defaults to project name if not specified in user's .env)
@@ -1110,6 +1241,36 @@ def build_project(
1110
1241
  )
1111
1242
  generator.generate()
1112
1243
 
1244
+ # Platform registration (only for prod builds)
1245
+ if build_env == "prod":
1246
+ console.print(
1247
+ "[dim]Registering with Golf platform and updating resources...[/dim]"
1248
+ )
1249
+ import asyncio
1250
+
1251
+ try:
1252
+ from golf.core.platform import register_project_with_platform
1253
+
1254
+ asyncio.run(
1255
+ register_project_with_platform(
1256
+ project_path=project_path,
1257
+ settings=settings,
1258
+ components=generator.components,
1259
+ )
1260
+ )
1261
+ console.print("[green]✓ Platform registration completed[/green]")
1262
+ except ImportError:
1263
+ console.print(
1264
+ "[yellow]Warning: Platform registration module not available[/yellow]"
1265
+ )
1266
+ except Exception as e:
1267
+ console.print(
1268
+ f"[yellow]Warning: Platform registration failed: {e}[/yellow]"
1269
+ )
1270
+ console.print(
1271
+ "[yellow]Tip: Ensure GOLF_API_KEY and GOLF_SERVER_ID are available in your .env file[/yellow]"
1272
+ )
1273
+
1113
1274
  # Create a simple README
1114
1275
  readme_content = f"""# {settings.name}
1115
1276
 
@@ -1130,7 +1291,7 @@ This is a standalone FastMCP server generated by GolfMCP.
1130
1291
 
1131
1292
  # Copy pyproject.toml with required dependencies
1132
1293
  base_dependencies = [
1133
- "fastmcp>=2.0.0",
1294
+ "fastmcp>=2.0.0,<2.6.0",
1134
1295
  "uvicorn>=0.20.0",
1135
1296
  "pydantic>=2.0.0",
1136
1297
  "python-dotenv>=1.0.0",
golf/core/builder_auth.py CHANGED
@@ -187,6 +187,11 @@ def generate_api_key_auth_components(
187
187
  " # Debug mode from environment",
188
188
  " debug = os.environ.get('GOLF_API_KEY_DEBUG', '').lower() == 'true'",
189
189
  " ",
190
+ " # Skip auth for monitoring endpoints",
191
+ " path = request.url.path",
192
+ " if path in ['/metrics', '/health']:",
193
+ " return await call_next(request)",
194
+ " ",
190
195
  " api_key_config = get_api_key_config()",
191
196
  " ",
192
197
  " if api_key_config:",