golf-mcp 0.1.7__py3-none-any.whl → 0.1.9__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/core/builder.py +240 -281
- golf/core/builder_auth.py +149 -43
- golf/core/builder_telemetry.py +44 -179
- golf/core/telemetry.py +46 -8
- golf/examples/api_key/.env.example +5 -0
- golf/examples/api_key/golf.json +3 -1
- golf/examples/basic/.env.example +3 -1
- golf/examples/basic/golf.json +3 -1
- golf/telemetry/__init__.py +19 -0
- golf/telemetry/instrumentation.py +540 -0
- {golf_mcp-0.1.7.dist-info → golf_mcp-0.1.9.dist-info}/METADATA +41 -2
- {golf_mcp-0.1.7.dist-info → golf_mcp-0.1.9.dist-info}/RECORD +17 -14
- {golf_mcp-0.1.7.dist-info → golf_mcp-0.1.9.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.7.dist-info → golf_mcp-0.1.9.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.7.dist-info → golf_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.7.dist-info → golf_mcp-0.1.9.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
620
|
-
|
|
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
|
-
#
|
|
623
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
658
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
751
|
-
# Order: imports, env_section,
|
|
752
|
-
#
|
|
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
|
-
|
|
757
|
-
server_code_lines +
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
#
|
|
831
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
|
|
964
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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 =
|
|
1103
|
-
if app_pos != -1:
|
|
1060
|
+
app_pos = server_code_content.find(app_marker)
|
|
1061
|
+
if app_pos != -1:
|
|
1104
1062
|
modified_code = (
|
|
1105
|
-
|
|
1106
|
-
auth_routes_code + "\n\n" +
|
|
1107
|
-
|
|
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]:
|
|
@@ -1161,9 +1116,13 @@ def build_import_map(project_path: Path, common_files: Dict[str, Path]) -> Dict[
|
|
|
1161
1116
|
try:
|
|
1162
1117
|
rel_to_component = dir_path.relative_to(component_type)
|
|
1163
1118
|
# Create the new import path
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1119
|
+
if str(rel_to_component) == ".":
|
|
1120
|
+
# This is at the root of the component type
|
|
1121
|
+
new_path = f"components.{component_type}"
|
|
1122
|
+
else:
|
|
1123
|
+
# Replace path separators with dots
|
|
1124
|
+
path_parts = str(rel_to_component).replace("\\", "/").split("/")
|
|
1125
|
+
new_path = f"components.{component_type}.{'.'.join(path_parts)}"
|
|
1167
1126
|
|
|
1168
1127
|
# Map both the directory and the common file
|
|
1169
1128
|
orig_module = dir_path_str
|