golf-mcp 0.1.6__py3-none-any.whl → 0.1.8__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/core/builder.py CHANGED
@@ -10,7 +10,6 @@ from typing import Any, Dict, List, Optional, Set
10
10
  import black
11
11
  from rich.console import Console
12
12
  from rich.progress import Progress, SpinnerColumn, TextColumn
13
- from rich.panel import Panel
14
13
 
15
14
  from golf.core.config import Settings
16
15
  from golf.core.parser import (
@@ -21,10 +20,8 @@ from golf.core.parser import (
21
20
  from golf.core.transformer import transform_component
22
21
  from golf.core.builder_auth import generate_auth_code, generate_auth_routes
23
22
  from golf.auth import get_auth_config
24
- from golf.auth import get_access_token
25
23
  from golf.core.builder_telemetry import (
26
- generate_otel_lifespan_code,
27
- generate_otel_instrumentation_code,
24
+ generate_telemetry_imports,
28
25
  get_otel_dependencies
29
26
  )
30
27
 
@@ -513,24 +510,40 @@ class CodeGenerator:
513
510
  """Generate the main server entry point."""
514
511
  server_file = self.output_dir / "server.py"
515
512
 
513
+ # Get auth components
514
+ provider_config, _ = get_auth_config()
515
+ auth_components = generate_auth_code(
516
+ server_name=self.settings.name,
517
+ host=self.settings.host,
518
+ port=self.settings.port,
519
+ https=False, # This could be configurable in settings
520
+ opentelemetry_enabled=self.settings.opentelemetry_enabled,
521
+ transport=self.settings.transport
522
+ )
523
+
516
524
  # Create imports section
517
525
  imports = [
518
526
  "from fastmcp import FastMCP",
519
527
  "import os",
520
528
  "import sys",
521
529
  "from dotenv import load_dotenv",
530
+ "import logging",
531
+ "",
532
+ "# Suppress FastMCP INFO logs",
533
+ "logging.getLogger('fastmcp').setLevel(logging.WARNING)",
534
+ "logging.getLogger('mcp').setLevel(logging.WARNING)",
522
535
  ""
523
536
  ]
524
537
 
525
- # For imports
538
+ # Add auth imports if auth is configured
539
+ if auth_components.get("has_auth"):
540
+ imports.extend(auth_components["imports"])
541
+ imports.append("")
542
+
543
+ # Add OpenTelemetry imports if enabled
526
544
  if self.settings.opentelemetry_enabled:
527
- imports.extend([
528
- "# OpenTelemetry imports",
529
- "from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
530
- "from starlette.middleware import Middleware",
531
- # otel_lifespan function will be defined from generate_otel_lifespan_code
532
- ])
533
- imports.append("") # Add blank line after all component type imports or OTel imports
545
+ imports.extend(generate_telemetry_imports())
546
+ imports.append("")
534
547
 
535
548
  # Add imports section for different transport methods
536
549
  if self.settings.transport == "sse":
@@ -539,14 +552,6 @@ class CodeGenerator:
539
552
  elif self.settings.transport != "stdio":
540
553
  imports.append("import uvicorn")
541
554
 
542
- # Create a new FastMCP instance for the server
543
- server_code_lines = ["# Create FastMCP server"]
544
- mcp_constructor_args = [f'"{self.settings.name}"']
545
-
546
- mcp_instance_line = f"mcp = FastMCP({', '.join(mcp_constructor_args)})"
547
- server_code_lines.append(mcp_instance_line)
548
- server_code_lines.append("")
549
-
550
555
  # Get transport-specific configuration
551
556
  transport_config = self._get_transport_config(self.settings.transport)
552
557
  endpoint_path = transport_config["endpoint_path"]
@@ -616,62 +621,78 @@ class CodeGenerator:
616
621
  imports.append(f"import {full_module_path}")
617
622
 
618
623
  # Add code to register this component
619
- if component_type == ComponentType.TOOL:
620
- registration = f"# Register the tool '{component.name}' from {full_module_path}"
624
+ if self.settings.opentelemetry_enabled:
625
+ # Use telemetry instrumentation
626
+ registration = f"# Register the {component_type.value} '{component.name}' with telemetry"
627
+ entry_func = component.entry_function if hasattr(component, "entry_function") and component.entry_function else "export"
621
628
 
622
- # Use the entry_function if available, otherwise try the export variable
623
- if hasattr(component, "entry_function") and component.entry_function:
624
- registration += f"\nmcp.add_tool({full_module_path}.{component.entry_function}"
625
- else:
626
- registration += f"\nmcp.add_tool({full_module_path}.export"
629
+ # Debug: Add logging to verify wrapping
630
+ registration += f"\n_wrapped_func = instrument_{component_type.value}({full_module_path}.{entry_func}, '{component.name}')"
627
631
 
628
- # Add the name parameter
629
- registration += f", name=\"{component.name}\""
630
-
631
- # Add description from docstring
632
- if component.docstring:
633
- # Escape any quotes in the docstring
634
- escaped_docstring = component.docstring.replace("\"", "\\\"")
635
- registration += f", description=\"{escaped_docstring}\""
636
- registration += ")"
632
+ if component_type == ComponentType.TOOL:
633
+ registration += f"\nmcp.add_tool(_wrapped_func, name=\"{component.name}\", description=\"{component.docstring or ''}\")"
634
+ elif component_type == ComponentType.RESOURCE:
635
+ registration += f"\nmcp.add_resource_fn(_wrapped_func, uri=\"{component.uri_template}\", name=\"{component.name}\", description=\"{component.docstring or ''}\")"
636
+ else: # PROMPT
637
+ registration += f"\nmcp.add_prompt(_wrapped_func, name=\"{component.name}\", description=\"{component.docstring or ''}\")"
638
+ else:
639
+ # Standard registration without telemetry
640
+ if component_type == ComponentType.TOOL:
641
+ registration = f"# Register the tool '{component.name}' from {full_module_path}"
642
+
643
+ # Use the entry_function if available, otherwise try the export variable
644
+ if hasattr(component, "entry_function") and component.entry_function:
645
+ registration += f"\nmcp.add_tool({full_module_path}.{component.entry_function}"
646
+ else:
647
+ registration += f"\nmcp.add_tool({full_module_path}.export"
648
+
649
+ # Add the name parameter
650
+ registration += f", name=\"{component.name}\""
651
+
652
+ # Add description from docstring
653
+ if component.docstring:
654
+ # Escape any quotes in the docstring
655
+ escaped_docstring = component.docstring.replace("\"", "\\\"")
656
+ registration += f", description=\"{escaped_docstring}\""
657
+ registration += ")"
637
658
 
638
- elif component_type == ComponentType.RESOURCE:
639
- registration = f"# Register the resource '{component.name}' from {full_module_path}"
640
-
641
- # Use the entry_function if available, otherwise try the export variable
642
- if hasattr(component, "entry_function") and component.entry_function:
643
- registration += f"\nmcp.add_resource_fn({full_module_path}.{component.entry_function}, uri=\"{component.uri_template}\""
644
- else:
645
- registration += f"\nmcp.add_resource_fn({full_module_path}.export, uri=\"{component.uri_template}\""
646
-
647
- # Add the name parameter
648
- registration += f", name=\"{component.name}\""
659
+ elif component_type == ComponentType.RESOURCE:
660
+ registration = f"# Register the resource '{component.name}' from {full_module_path}"
649
661
 
650
- # Add description from docstring
651
- if component.docstring:
652
- # Escape any quotes in the docstring
653
- escaped_docstring = component.docstring.replace("\"", "\\\"")
654
- registration += f", description=\"{escaped_docstring}\""
655
- registration += ")"
662
+ # Use the entry_function if available, otherwise try the export variable
663
+ if hasattr(component, "entry_function") and component.entry_function:
664
+ registration += f"\nmcp.add_resource_fn({full_module_path}.{component.entry_function}, uri=\"{component.uri_template}\""
665
+ else:
666
+ registration += f"\nmcp.add_resource_fn({full_module_path}.export, uri=\"{component.uri_template}\""
667
+
668
+ # Add the name parameter
669
+ registration += f", name=\"{component.name}\""
670
+
671
+ # Add description from docstring
672
+ if component.docstring:
673
+ # Escape any quotes in the docstring
674
+ escaped_docstring = component.docstring.replace("\"", "\\\"")
675
+ registration += f", description=\"{escaped_docstring}\""
676
+ registration += ")"
656
677
 
657
- else: # PROMPT
658
- registration = f"# Register the prompt '{component.name}' from {full_module_path}"
659
-
660
- # Use the entry_function if available, otherwise try the export variable
661
- if hasattr(component, "entry_function") and component.entry_function:
662
- registration += f"\nmcp.add_prompt({full_module_path}.{component.entry_function}"
663
- else:
664
- registration += f"\nmcp.add_prompt({full_module_path}.export"
665
-
666
- # Add the name parameter
667
- registration += f", name=\"{component.name}\""
678
+ else: # PROMPT
679
+ registration = f"# Register the prompt '{component.name}' from {full_module_path}"
668
680
 
669
- # Add description from docstring
670
- if component.docstring:
671
- # Escape any quotes in the docstring
672
- escaped_docstring = component.docstring.replace("\"", "\\\"")
673
- registration += f", description=\"{escaped_docstring}\""
674
- registration += ")"
681
+ # Use the entry_function if available, otherwise try the export variable
682
+ if hasattr(component, "entry_function") and component.entry_function:
683
+ registration += f"\nmcp.add_prompt({full_module_path}.{component.entry_function}"
684
+ else:
685
+ registration += f"\nmcp.add_prompt({full_module_path}.export"
686
+
687
+ # Add the name parameter
688
+ registration += f", name=\"{component.name}\""
689
+
690
+ # Add description from docstring
691
+ if component.docstring:
692
+ # Escape any quotes in the docstring
693
+ escaped_docstring = component.docstring.replace("\"", "\\\"")
694
+ registration += f", description=\"{escaped_docstring}\""
695
+ registration += ")"
675
696
 
676
697
  component_registrations.append(registration)
677
698
 
@@ -688,21 +709,37 @@ class CodeGenerator:
688
709
  ""
689
710
  ]
690
711
 
691
- # After env_section, add OpenTelemetry lifespan code
692
- otel_definitions_code = []
693
- otel_instrumentation_application_code = [] # For instrumentation that runs after mcp is set up
712
+ # OpenTelemetry setup code will be handled through imports and lifespan
694
713
 
714
+ # Add auth setup code if auth is configured
715
+ auth_setup_code = []
716
+ if auth_components.get("has_auth"):
717
+ auth_setup_code = auth_components["setup_code"]
718
+
719
+ # Create FastMCP instance section
720
+ server_code_lines = ["# Create FastMCP server"]
721
+
722
+ # Build FastMCP constructor arguments
723
+ mcp_constructor_args = [f'"{self.settings.name}"']
724
+
725
+ # Add auth arguments if configured
726
+ if auth_components.get("has_auth") and auth_components.get("fastmcp_args"):
727
+ for key, value in auth_components["fastmcp_args"].items():
728
+ mcp_constructor_args.append(f"{key}={value}")
729
+
730
+ # Add OpenTelemetry parameters if enabled
695
731
  if self.settings.opentelemetry_enabled:
696
- otel_definitions_code.append(generate_otel_lifespan_code(
697
- default_exporter=self.settings.opentelemetry_default_exporter,
698
- project_name=self.settings.name
699
- ))
700
- otel_definitions_code.append("") # Add blank line
732
+ mcp_constructor_args.append("lifespan=telemetry_lifespan")
701
733
 
702
- # Prepare instrumentation code to be added after component registration
703
- otel_instrumentation_application_code.append("# Apply OpenTelemetry Instrumentation")
704
- otel_instrumentation_application_code.append(generate_otel_instrumentation_code())
705
- otel_instrumentation_application_code.append("")
734
+ mcp_instance_line = f"mcp = FastMCP({', '.join(mcp_constructor_args)})"
735
+ server_code_lines.append(mcp_instance_line)
736
+ server_code_lines.append("")
737
+
738
+ # Add any post-init code from auth
739
+ post_init_code = []
740
+ if auth_components.get("has_auth") and auth_components.get("post_init_code"):
741
+ post_init_code.extend(auth_components["post_init_code"])
742
+ post_init_code.append("")
706
743
 
707
744
  # Main entry point with transport-specific app initialization
708
745
  main_code = [
@@ -729,34 +766,42 @@ class CodeGenerator:
729
766
  if self.settings.transport == "sse":
730
767
  main_code.extend([
731
768
  " # For SSE, FastMCP's run method handles auth integration better",
732
- " print(f\"[Server Runner] Using mcp.run() for SSE transport with host={host}, port={port}\", file=sys.stderr)",
733
- " mcp.run(transport=\"sse\", host=host, port=port, log_level=\"debug\")"
769
+ " mcp.run(transport=\"sse\", host=host, port=port, log_level=\"info\")"
734
770
  ])
735
771
  elif self.settings.transport == "streamable-http":
736
772
  main_code.extend([
737
773
  " # Create HTTP app and run with uvicorn",
738
- " print(f\"[Server Runner] Starting streamable-http transport with host={host}, port={port}\", file=sys.stderr)",
739
774
  " app = mcp.http_app()",
740
- " uvicorn.run(app, host=host, port=port, log_level=\"debug\")"
775
+ ])
776
+
777
+ # Add OpenTelemetry middleware to the HTTP app if enabled
778
+ if self.settings.opentelemetry_enabled:
779
+ main_code.extend([
780
+ " # Apply OpenTelemetry middleware to the HTTP app",
781
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
782
+ " app = OpenTelemetryMiddleware(app)",
783
+ ])
784
+
785
+ main_code.extend([
786
+ " uvicorn.run(app, host=host, port=port, log_level=\"info\")"
741
787
  ])
742
788
  else:
743
789
  # For stdio transport, use mcp.run()
744
790
  main_code.extend([
745
791
  " # Run with stdio transport",
746
- " print(f\"[Server Runner] Starting stdio transport\", file=sys.stderr)",
747
792
  " mcp.run(transport=\"stdio\")"
748
793
  ])
749
794
 
750
- # Combine all sections - move env_section to right location
751
- # Order: imports, env_section, otel_definitions (lifespan func), server_code (mcp init),
752
- # component_registrations, otel_instrumentation (wrappers), main_code (run block)
795
+ # Combine all sections
796
+ # Order: imports, env_section, auth_setup, server_code (mcp init),
797
+ # post_init (API key middleware), component_registrations, main_code (run block)
753
798
  code = "\n".join(
754
799
  imports +
755
800
  env_section +
756
- otel_definitions_code +
757
- server_code_lines + # Add back the server_code_lines with constructor
758
- component_registrations +
759
- otel_instrumentation_application_code + # Added instrumentation here
801
+ auth_setup_code +
802
+ server_code_lines +
803
+ post_init_code +
804
+ component_registrations +
760
805
  main_code
761
806
  )
762
807
 
@@ -826,15 +871,64 @@ def build_project(
826
871
  if output_dir.exists():
827
872
  shutil.rmtree(output_dir)
828
873
  output_dir.mkdir(parents=True, exist_ok=True) # Ensure output_dir exists after clearing
829
-
830
- # If dev build and copy_env flag is true, copy .env file from project root to output_dir
831
- if copy_env: # The build_env string ('dev'/'prod') check can be done in the CLI layer that sets copy_env
874
+
875
+ # --- BEGIN Enhanced .env handling ---
876
+ env_vars_to_write = {}
877
+ env_file_path = output_dir / ".env"
878
+
879
+ # 1. Load from existing project .env if copy_env is true
880
+ if copy_env:
832
881
  project_env_file = project_path / ".env"
833
882
  if project_env_file.exists():
834
883
  try:
835
- shutil.copy(project_env_file, output_dir / ".env")
884
+ from dotenv import dotenv_values
885
+ env_vars_to_write.update(dotenv_values(project_env_file))
886
+ except ImportError:
887
+ console.print("[yellow]Warning: python-dotenv is not installed. Cannot read existing .env file for rich merging. Copying directly.[/yellow]")
888
+ try:
889
+ shutil.copy(project_env_file, env_file_path)
890
+ # If direct copy happens, re-read for step 2 & 3 to respect its content
891
+ if env_file_path.exists():
892
+ from dotenv import dotenv_values
893
+ env_vars_to_write.update(dotenv_values(env_file_path)) # Read what was copied
894
+ except Exception as e:
895
+ console.print(f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]")
836
896
  except Exception as e:
837
- console.print(f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]")
897
+ console.print(f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]")
898
+
899
+
900
+ # 2. Apply Golf's OTel default exporter setting if OTEL_TRACES_EXPORTER is not already set
901
+ if settings.opentelemetry_enabled and settings.opentelemetry_default_exporter:
902
+ if "OTEL_TRACES_EXPORTER" not in env_vars_to_write:
903
+ env_vars_to_write["OTEL_TRACES_EXPORTER"] = settings.opentelemetry_default_exporter
904
+ console.print(f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]")
905
+
906
+ # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
907
+ # (Ensures service name defaults to project name if not specified in user's .env)
908
+ if settings.opentelemetry_enabled and settings.name:
909
+ if "OTEL_SERVICE_NAME" not in env_vars_to_write:
910
+ env_vars_to_write["OTEL_SERVICE_NAME"] = settings.name
911
+
912
+ # 4. (Re-)Write the .env file in the output directory if there's anything to write
913
+ if env_vars_to_write:
914
+ try:
915
+ with open(env_file_path, "w") as f:
916
+ for key, value in env_vars_to_write.items():
917
+ # Ensure values are properly quoted if they contain spaces or special characters
918
+ # and handle existing quotes within the value.
919
+ if isinstance(value, str):
920
+ # Replace backslashes first, then double quotes
921
+ processed_value = value.replace('\\', '\\\\') # Escape backslashes
922
+ processed_value = processed_value.replace('"', '\\"') # Escape double quotes
923
+ if ' ' in value or '#' in value or '\n' in value or '"' in value or "'" in value:
924
+ f.write(f'{key}="{processed_value}"\n')
925
+ else:
926
+ f.write(f"{key}={processed_value}\n")
927
+ else: # For non-string values, write directly
928
+ f.write(f"{key}={value}\n")
929
+ except Exception as e:
930
+ console.print(f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]")
931
+ # --- END Enhanced .env handling ---
838
932
 
839
933
  # Show what we're building, with environment info
840
934
  console.print(f"[bold]Building [green]{settings.name}[/green] ({build_env} environment)[/bold]")
@@ -918,11 +1012,12 @@ dependencies = [
918
1012
 
919
1013
  from golf.auth.provider import ProviderConfig
920
1014
  from golf.auth.oauth import GolfOAuthProvider, create_callback_handler
921
- from golf.auth.helpers import get_access_token, get_provider_token, extract_token_from_header
1015
+ from golf.auth.helpers import get_access_token, get_provider_token, extract_token_from_header, get_api_key, set_api_key
1016
+ from golf.auth.api_key import configure_api_key, get_api_key_config
922
1017
  """)
923
1018
 
924
1019
  # Copy provider, oauth, and helper modules
925
- for module in ["provider.py", "oauth.py", "helpers.py"]:
1020
+ for module in ["provider.py", "oauth.py", "helpers.py", "api_key.py"]:
926
1021
  src_file = Path(__file__).parent.parent.parent / "golf" / "auth" / module
927
1022
  dst_file = auth_dir / module
928
1023
 
@@ -931,197 +1026,57 @@ from golf.auth.helpers import get_access_token, get_provider_token, extract_toke
931
1026
  else:
932
1027
  console.print(f"[yellow]Warning: Could not find {src_file} to copy[/yellow]")
933
1028
 
934
- # Now handle the auth integration if configured
935
- if provider_config:
936
-
937
- # Generate the auth code to inject into server.py
938
- # The existing call to generate_auth_code.
939
- # We need to ensure the arguments passed are sensible.
940
- # server.py determines issuer_url at runtime. generate_auth_code
941
- # likely uses host/port/https to construct its own version or parts of it.
942
-
943
- # Determine protocol for https flag based on runtime logic similar to server.py
944
- # This is a bit of a guess as settings doesn't explicitly store protocol for generate_auth_code
945
- # A small inconsistency here if server.py's runtime logic for issuer_url differs significantly
946
- # from what generate_auth_code expects/builds.
947
- # For now, let's assume False is okay, or it's handled internally by generate_auth_code
948
- # based on typical dev environments.
949
- is_https_proto = False # Default, adjust if settings provide this info for build time
950
-
951
- auth_code_str = generate_auth_code( # Renamed to auth_code_str to avoid confusion
952
- server_name=settings.name,
953
- host=settings.host,
954
- port=settings.port
955
- )
956
- else:
957
- # If auth is not configured, create a basic FastMCP instantiation string
958
- # This string will then be processed for OTel args like the auth_code_str would be
959
- auth_code_str = f"mcp = FastMCP('{settings.name}')"
960
-
961
- # ---- Centralized OpenTelemetry Argument Injection ----
1029
+ # Copy telemetry module if OpenTelemetry is enabled
962
1030
  if settings.opentelemetry_enabled:
963
- temp_mcp_lines = auth_code_str.split('\n')
964
- final_mcp_lines = []
965
- otel_args_injected = False
966
- for line_content in temp_mcp_lines:
967
- if "mcp = FastMCP(" in line_content and ")" in line_content and not otel_args_injected:
968
- open_paren_pos = line_content.find("(")
969
- close_paren_pos = line_content.rfind(")")
970
- if open_paren_pos != -1 and close_paren_pos != -1 and open_paren_pos < close_paren_pos:
971
- existing_args_str = line_content[open_paren_pos+1:close_paren_pos].strip()
972
- otel_args_to_add = []
973
- if "lifespan=" not in existing_args_str:
974
- otel_args_to_add.append("lifespan=otel_lifespan")
975
- if settings.transport != "stdio" and "middleware=" not in existing_args_str:
976
- otel_args_to_add.append("middleware=[Middleware(OpenTelemetryMiddleware)]")
977
-
978
- if otel_args_to_add:
979
- new_args_str = existing_args_str
980
- if new_args_str and not new_args_str.endswith(','):
981
- new_args_str += ", "
982
- new_args_str += ", ".join(otel_args_to_add)
983
- new_line = f"{line_content[:open_paren_pos+1]}{new_args_str}{line_content[close_paren_pos:]}"
984
- final_mcp_lines.append(new_line)
985
- otel_args_injected = True
986
- continue
987
- final_mcp_lines.append(line_content)
1031
+ telemetry_dir = output_dir / "golf" / "telemetry"
1032
+ telemetry_dir.mkdir(parents=True, exist_ok=True)
988
1033
 
989
- if otel_args_injected:
990
- auth_code_str = "\n".join(final_mcp_lines)
991
- elif otel_args_to_add: # Only warn if we actually tried to add something
992
- console.print(f"[yellow]Warning: Could not automatically inject OpenTelemetry lifespan/middleware into FastMCP constructor. Review server.py.[/yellow]")
993
- # ---- END Centralized OpenTelemetry Argument Injection ----
994
-
995
- # ---- MODIFICATION TO auth_code_str for _set_active_golf_oauth_provider (if auth was enabled) ----
996
- if provider_config: # Only run this if auth was actually processed by generate_auth_code
1034
+ # Copy telemetry __init__.py
1035
+ src_init = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1036
+ dst_init = telemetry_dir / "__init__.py"
1037
+ if src_init.exists():
1038
+ shutil.copy(src_init, dst_init)
1039
+
1040
+ # Copy instrumentation module
1041
+ src_instrumentation = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "instrumentation.py"
1042
+ dst_instrumentation = telemetry_dir / "instrumentation.py"
1043
+ if src_instrumentation.exists():
1044
+ shutil.copy(src_instrumentation, dst_instrumentation)
1045
+ else:
1046
+ console.print("[yellow]Warning: Could not find telemetry instrumentation module[/yellow]")
1047
+
1048
+ # Check if auth routes need to be added
1049
+ provider_config, _ = get_auth_config()
1050
+ if provider_config:
997
1051
  auth_routes_code = generate_auth_routes()
998
-
1052
+
999
1053
  server_file = output_dir / "server.py"
1000
1054
  if server_file.exists():
1001
1055
  with open(server_file, "r") as f:
1002
1056
  server_code_content = f.read()
1003
1057
 
1004
- create_marker = '# Create FastMCP server'
1005
- # The original logic replaces the FastMCP instantiation part.
1006
- # So we use the modified auth_code_str here.
1007
- create_pos = server_code_content.find(create_marker)
1008
- if create_pos != -1: # Ensure marker is found
1009
- create_pos += len(create_marker) # Move past the marker text itself
1010
- create_next_line = server_code_content.find('\n', create_pos) + 1
1011
- # Assuming the original mcp = FastMCP(...) line is what auth_code_str replaces
1012
- # Find the end of the line that starts with "mcp = FastMCP("
1013
- mcp_line_start_search = server_code_content.find("mcp = FastMCP(", create_next_line)
1014
- if mcp_line_start_search != -1:
1015
- mcp_line_end = server_code_content.find('\n', mcp_line_start_search)
1016
- if mcp_line_end == -1: mcp_line_end = len(server_code_content) # if it's the last line
1017
-
1018
- modified_code = (
1019
- server_code_content[:create_next_line] +
1020
- auth_code_str + # Use the modified auth code string
1021
- server_code_content[mcp_line_end:]
1022
- )
1023
- else: # Fallback if "mcp = FastMCP(" line isn't found as expected
1024
- console.print(f"[yellow]Warning: Could not precisely find 'mcp = FastMCP(...)' line for replacement by auth_code in {server_file}. Appending auth_code instead.[/yellow]")
1025
- # This part of the logic was to replace mcp = FastMCP(...)
1026
- # If the generate_auth_code ALREADY includes the mcp = FastMCP(...) line,
1027
- # then the original injection logic might be different.
1028
- # The example server.py shows that the auth_code INCLUDES the mcp = FastMCP(...) line.
1029
- # The original code in builder.py:
1030
- # create_next_line = server_code.find('\n', create_pos) + 1
1031
- # mcp_line_end = server_code.find('\n', create_next_line)
1032
- # This implies it replaces ONE line after '# Create FastMCP server'
1033
- # This needs to be robust. If auth_code_str contains the `mcp = FastMCP(...)` line itself,
1034
- # then this replacement logic is correct.
1035
-
1036
- # The server.py example from `new/dist` implies that auth_code effectively *is* the
1037
- # whole block from "import os" for auth settings down to and including "mcp = FastMCP(...)".
1038
- # The original `_generate_server` creates a very minimal `mcp = FastMCP(...)`.
1039
- # The `build_project` then overwrites this with the richer `auth_code` block.
1040
- # Let's assume `auth_code_str_modified` should replace from `create_next_line` up to
1041
- # where the original `mcp = FastMCP(...)` definition ended.
1042
-
1043
- # Re-evaluating the injection for auth_code_str_modified.
1044
- # The server.py is first generated by _generate_server.
1045
- # Then, if auth is enabled, this part of build_project MODIFIES it.
1046
- # It finds '# Create FastMCP server', then replaces the *next line* (which is `mcp = FastMCP(...)` from _generate_server)
1047
- # with the entire `auth_code_str_modified`.
1048
-
1049
- # Original line to find/replace: `mcp = FastMCP("{self.settings.name}")`
1050
- # OR if telemetry was on `mcp = FastMCP("{self.settings.name}", lifespan=otel_lifespan)`
1051
- # The replacement logic must be robust to find the line created by _generate_server
1052
-
1053
- # Let's find the line starting with "mcp = FastMCP(" that _generate_server created
1054
- original_mcp_instantiation_pattern = "mcp = FastMCP("
1055
- start_replace_idx = server_code_content.find(original_mcp_instantiation_pattern)
1056
-
1057
- if start_replace_idx != -1:
1058
- # We need to find the complete statement, including any continuation lines
1059
- line_start = server_code_content.rfind('\n', 0, start_replace_idx) + 1
1060
-
1061
- # Find the closing parenthesis, handling potential multi-line calls
1062
- opening_paren_pos = server_code_content.find('(', start_replace_idx)
1063
- if opening_paren_pos != -1:
1064
- # Count open parentheses to handle nested ones correctly
1065
- paren_count = 1
1066
- pos = opening_paren_pos + 1
1067
- while pos < len(server_code_content) and paren_count > 0:
1068
- if server_code_content[pos] == '(':
1069
- paren_count += 1
1070
- elif server_code_content[pos] == ')':
1071
- paren_count -= 1
1072
- pos += 1
1073
-
1074
- closing_paren_pos = pos - 1 if paren_count == 0 else -1
1075
-
1076
- if closing_paren_pos != -1:
1077
- # Find the end of the statement (newline after the closing parenthesis)
1078
- next_newline = server_code_content.find('\n', closing_paren_pos)
1079
- if next_newline != -1:
1080
- end_replace_idx = next_newline + 1
1081
- else:
1082
- end_replace_idx = len(server_code_content)
1083
-
1084
- # Replace the entire statement with the auth code
1085
- modified_code = (
1086
- server_code_content[:line_start] +
1087
- auth_code_str +
1088
- server_code_content[end_replace_idx:]
1089
- )
1090
- else:
1091
- console.print(f"[red]Error: Could not find closing parenthesis for FastMCP constructor in {server_file}. Auth injection may fail.[/red]")
1092
- modified_code = server_code_content
1093
- else:
1094
- console.print(f"[red]Error: Could not find opening parenthesis for FastMCP constructor in {server_file}. Auth injection may fail.[/red]")
1095
- modified_code = server_code_content
1096
-
1097
- else: # create_marker not found (This case should ideally not happen if _generate_server works)
1098
- console.print(f"[red]Could not find injection marker '{create_marker}' in {server_file}. Auth injection failed.[/red]")
1099
- modified_code = server_code_content # No change
1100
-
1058
+ # Add auth routes before the main block
1101
1059
  app_marker = 'if __name__ == "__main__":'
1102
- app_pos = modified_code.find(app_marker)
1103
- if app_pos != -1: # Ensure marker is found
1060
+ app_pos = server_code_content.find(app_marker)
1061
+ if app_pos != -1:
1104
1062
  modified_code = (
1105
- modified_code[:app_pos] +
1106
- auth_routes_code + "\n\n" + # Ensure auth routes are injected
1107
- modified_code[app_pos:]
1063
+ server_code_content[:app_pos] +
1064
+ auth_routes_code + "\n\n" +
1065
+ server_code_content[app_pos:]
1108
1066
  )
1067
+
1068
+ # Format with black before writing
1069
+ try:
1070
+ final_code_to_write = black.format_str(modified_code, mode=black.Mode())
1071
+ except Exception as e:
1072
+ console.print(f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]")
1073
+ final_code_to_write = modified_code
1074
+
1075
+ with open(server_file, "w") as f:
1076
+ f.write(final_code_to_write)
1109
1077
  else:
1110
1078
  console.print(f"[yellow]Warning: Could not find main block marker '{app_marker}' in {server_file} to inject auth routes.[/yellow]")
1111
1079
 
1112
- # Format with black before writing
1113
- try:
1114
- final_code_to_write = black.format_str(modified_code, mode=black.Mode())
1115
- except Exception as e:
1116
- console.print(f"[yellow]Warning: Could not format server.py after auth injection: {e}[/yellow]")
1117
- final_code_to_write = modified_code # Write unformatted if black fails
1118
-
1119
- with open(server_file, "w") as f:
1120
- f.write(final_code_to_write)
1121
-
1122
- else: # server_file does not exist
1123
- console.print(f"[red]Error: {server_file} does not exist for auth modification. Ensure _generate_server runs first.[/red]")
1124
-
1125
1080
 
1126
1081
  # Renamed function - was find_shared_modules
1127
1082
  def find_common_files(project_path: Path, components: Dict[ComponentType, List[ParsedComponent]]) -> Dict[str, Path]: