Coverage for /Users/antonigmitruk/golf/src/golf/core/builder.py: 0%

588 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-08-16 18:46 +0200

1"""Builder for generating FastMCP manifests from parsed components.""" 

2 

3import json 

4import os 

5import shutil 

6import sys 

7from pathlib import Path 

8from typing import Any 

9 

10import black 

11from rich.console import Console 

12 

13from golf.auth import is_auth_configured 

14from golf.auth.api_key import get_api_key_config 

15from golf.core.builder_auth import generate_auth_code, generate_auth_routes 

16from golf.core.builder_telemetry import ( 

17 generate_telemetry_imports, 

18) 

19from golf.cli.branding import create_build_header, get_status_text, STATUS_ICONS, GOLF_BLUE 

20from golf.core.config import Settings 

21from golf.core.parser import ( 

22 ComponentType, 

23 ParsedComponent, 

24 parse_project, 

25) 

26from golf.core.transformer import transform_component 

27 

28console = Console() 

29 

30 

31class ManifestBuilder: 

32 """Builds FastMCP manifest from parsed components.""" 

33 

34 def __init__(self, project_path: Path, settings: Settings) -> None: 

35 """Initialize the manifest builder. 

36 

37 Args: 

38 project_path: Path to the project root 

39 settings: Project settings 

40 """ 

41 self.project_path = project_path 

42 self.settings = settings 

43 self.components: dict[ComponentType, list[ParsedComponent]] = {} 

44 self.manifest: dict[str, Any] = { 

45 "name": settings.name, 

46 "description": settings.description or "", 

47 "tools": [], 

48 "resources": [], 

49 "prompts": [], 

50 } 

51 

52 def build(self) -> dict[str, Any]: 

53 """Build the complete manifest. 

54 

55 Returns: 

56 FastMCP manifest dictionary 

57 """ 

58 # Parse all components 

59 self.components = parse_project(self.project_path) 

60 

61 # Process each component type 

62 self._process_tools() 

63 self._process_resources() 

64 self._process_prompts() 

65 

66 return self.manifest 

67 

68 def _process_tools(self) -> None: 

69 """Process all tool components and add them to the manifest.""" 

70 for component in self.components[ComponentType.TOOL]: 

71 # Extract the properties directly from the Input schema if it exists 

72 input_properties = {} 

73 required_fields = [] 

74 

75 if component.input_schema and "properties" in component.input_schema: 

76 input_properties = component.input_schema["properties"] 

77 # Get required fields if they exist 

78 if "required" in component.input_schema: 

79 required_fields = component.input_schema["required"] 

80 

81 # Create a flattened tool schema matching FastMCP documentation examples 

82 tool_schema = { 

83 "name": component.name, 

84 "description": component.docstring or "", 

85 "inputSchema": { 

86 "type": "object", 

87 "properties": input_properties, 

88 "additionalProperties": False, 

89 "$schema": "http://json-schema.org/draft-07/schema#", 

90 }, 

91 "annotations": {"title": component.name.replace("-", " ").title()}, 

92 "entry_function": component.entry_function, 

93 } 

94 

95 # Include required fields if they exist 

96 if required_fields: 

97 tool_schema["inputSchema"]["required"] = required_fields 

98 

99 # Add tool annotations if present 

100 if component.annotations: 

101 # Merge with existing annotations (keeping title) 

102 tool_schema["annotations"].update(component.annotations) 

103 

104 # Add the tool to the manifest 

105 self.manifest["tools"].append(tool_schema) 

106 

107 def _process_resources(self) -> None: 

108 """Process all resource components and add them to the manifest.""" 

109 for component in self.components[ComponentType.RESOURCE]: 

110 if not component.uri_template: 

111 console.print(f"[yellow]Warning: Resource {component.name} has no URI template[/yellow]") 

112 continue 

113 

114 resource_schema = { 

115 "uri": component.uri_template, 

116 "name": component.name, 

117 "description": component.docstring or "", 

118 "entry_function": component.entry_function, 

119 } 

120 

121 # Add the resource to the manifest 

122 self.manifest["resources"].append(resource_schema) 

123 

124 def _process_prompts(self) -> None: 

125 """Process all prompt components and add them to the manifest.""" 

126 for component in self.components[ComponentType.PROMPT]: 

127 # For prompts, the handler will have to load the module and execute 

128 # the run function 

129 # to get the actual messages, so we just register it by name 

130 prompt_schema = { 

131 "name": component.name, 

132 "description": component.docstring or "", 

133 "entry_function": component.entry_function, 

134 } 

135 

136 # If the prompt has parameters, include them 

137 if component.parameters: 

138 arguments = [] 

139 for param in component.parameters: 

140 arguments.append( 

141 {"name": param, "required": True} # Default to required 

142 ) 

143 prompt_schema["arguments"] = arguments 

144 

145 # Add the prompt to the manifest 

146 self.manifest["prompts"].append(prompt_schema) 

147 

148 def save_manifest(self, output_path: Path | None = None) -> Path: 

149 """Save the manifest to a JSON file. 

150 

151 Args: 

152 output_path: Path to save the manifest to (defaults to .golf/manifest.json) 

153 

154 Returns: 

155 Path where the manifest was saved 

156 """ 

157 if not output_path: 

158 # Create .golf directory if it doesn't exist 

159 golf_dir = self.project_path / ".golf" 

160 golf_dir.mkdir(exist_ok=True) 

161 output_path = golf_dir / "manifest.json" 

162 

163 # Ensure parent directories exist 

164 output_path.parent.mkdir(parents=True, exist_ok=True) 

165 

166 # Write the manifest to the file 

167 with open(output_path, "w") as f: 

168 json.dump(self.manifest, f, indent=2) 

169 

170 console.print(f"[green]Manifest saved to {output_path}[/green]") 

171 return output_path 

172 

173 

174def build_manifest(project_path: Path, settings: Settings) -> dict[str, Any]: 

175 """Build a FastMCP manifest from parsed components. 

176 

177 Args: 

178 project_path: Path to the project root 

179 settings: Project settings 

180 

181 Returns: 

182 FastMCP manifest dictionary 

183 """ 

184 # Use the ManifestBuilder class to build the manifest 

185 builder = ManifestBuilder(project_path, settings) 

186 return builder.build() 

187 

188 

189def compute_manifest_diff(old_manifest: dict[str, Any], new_manifest: dict[str, Any]) -> dict[str, Any]: 

190 """Compute the difference between two manifests. 

191 

192 Args: 

193 old_manifest: Previous manifest 

194 new_manifest: New manifest 

195 

196 Returns: 

197 Dictionary describing the changes 

198 """ 

199 diff = { 

200 "tools": {"added": [], "removed": [], "changed": []}, 

201 "resources": {"added": [], "removed": [], "changed": []}, 

202 "prompts": {"added": [], "removed": [], "changed": []}, 

203 } 

204 

205 # Helper function to extract names from a list of components 

206 def extract_names(components: list[dict[str, Any]]) -> set[str]: 

207 return {comp["name"] for comp in components} 

208 

209 # Compare tools 

210 old_tools = extract_names(old_manifest.get("tools", [])) 

211 new_tools = extract_names(new_manifest.get("tools", [])) 

212 diff["tools"]["added"] = list(new_tools - old_tools) 

213 diff["tools"]["removed"] = list(old_tools - new_tools) 

214 

215 # Compare tools that exist in both for changes 

216 for new_tool in new_manifest.get("tools", []): 

217 if new_tool["name"] in old_tools: 

218 # Find the corresponding old tool 

219 old_tool = next( 

220 (t for t in old_manifest.get("tools", []) if t["name"] == new_tool["name"]), 

221 None, 

222 ) 

223 if old_tool and json.dumps(old_tool) != json.dumps(new_tool): 

224 diff["tools"]["changed"].append(new_tool["name"]) 

225 

226 # Compare resources 

227 old_resources = extract_names(old_manifest.get("resources", [])) 

228 new_resources = extract_names(new_manifest.get("resources", [])) 

229 diff["resources"]["added"] = list(new_resources - old_resources) 

230 diff["resources"]["removed"] = list(old_resources - new_resources) 

231 

232 # Compare resources that exist in both for changes 

233 for new_resource in new_manifest.get("resources", []): 

234 if new_resource["name"] in old_resources: 

235 # Find the corresponding old resource 

236 old_resource = next( 

237 (r for r in old_manifest.get("resources", []) if r["name"] == new_resource["name"]), 

238 None, 

239 ) 

240 if old_resource and json.dumps(old_resource) != json.dumps(new_resource): 

241 diff["resources"]["changed"].append(new_resource["name"]) 

242 

243 # Compare prompts 

244 old_prompts = extract_names(old_manifest.get("prompts", [])) 

245 new_prompts = extract_names(new_manifest.get("prompts", [])) 

246 diff["prompts"]["added"] = list(new_prompts - old_prompts) 

247 diff["prompts"]["removed"] = list(old_prompts - new_prompts) 

248 

249 # Compare prompts that exist in both for changes 

250 for new_prompt in new_manifest.get("prompts", []): 

251 if new_prompt["name"] in old_prompts: 

252 # Find the corresponding old prompt 

253 old_prompt = next( 

254 (p for p in old_manifest.get("prompts", []) if p["name"] == new_prompt["name"]), 

255 None, 

256 ) 

257 if old_prompt and json.dumps(old_prompt) != json.dumps(new_prompt): 

258 diff["prompts"]["changed"].append(new_prompt["name"]) 

259 

260 return diff 

261 

262 

263def has_changes(diff: dict[str, Any]) -> bool: 

264 """Check if a manifest diff contains any changes. 

265 

266 Args: 

267 diff: Manifest diff from compute_manifest_diff 

268 

269 Returns: 

270 True if there are any changes, False otherwise 

271 """ 

272 for category in diff: 

273 for change_type in diff[category]: 

274 if diff[category][change_type]: 

275 return True 

276 

277 return False 

278 

279 

280class CodeGenerator: 

281 """Code generator for FastMCP applications.""" 

282 

283 def __init__( 

284 self, 

285 project_path: Path, 

286 settings: Settings, 

287 output_dir: Path, 

288 build_env: str = "prod", 

289 copy_env: bool = False, 

290 ) -> None: 

291 """Initialize the code generator. 

292 

293 Args: 

294 project_path: Path to the project root 

295 settings: Project settings 

296 output_dir: Directory to output the generated code 

297 build_env: Build environment ('dev' or 'prod') 

298 copy_env: Whether to copy environment variables to the built app 

299 """ 

300 self.project_path = project_path 

301 self.settings = settings 

302 self.output_dir = output_dir 

303 self.build_env = build_env 

304 self.copy_env = copy_env 

305 self.components = {} 

306 self.manifest = {} 

307 self.shared_files = {} 

308 self.import_map = {} 

309 

310 def generate(self) -> None: 

311 """Generate the FastMCP application code.""" 

312 # Parse the project and build the manifest 

313 with console.status("Analyzing project components..."): 

314 self.components = parse_project(self.project_path) 

315 self.manifest = build_manifest(self.project_path, self.settings) 

316 

317 # Find shared Python files and build import map 

318 from golf.core.parser import parse_shared_files 

319 self.shared_files = parse_shared_files(self.project_path) 

320 self.import_map = build_import_map(self.project_path, self.shared_files) 

321 

322 # Create output directory structure 

323 with console.status("Creating directory structure..."): 

324 self._create_directory_structure() 

325 

326 # Generate code for all components 

327 tasks = [ 

328 ("Generating tools", self._generate_tools), 

329 ("Generating resources", self._generate_resources), 

330 ("Generating prompts", self._generate_prompts), 

331 ("Generating server entry point", self._generate_server), 

332 ] 

333 

334 for description, func in tasks: 

335 console.print(get_status_text("generating", description)) 

336 func() 

337 

338 # Get relative path for display 

339 try: 

340 output_dir_display = self.output_dir.relative_to(Path.cwd()) 

341 except (ValueError, FileNotFoundError, OSError): 

342 # ValueError: paths don't have a common base 

343 # FileNotFoundError/OSError: current directory was deleted 

344 output_dir_display = self.output_dir 

345 

346 # Show success message with output directory 

347 console.print() 

348 console.print(get_status_text("success", f"Build completed successfully in {output_dir_display}")) 

349 

350 def _create_directory_structure(self) -> None: 

351 """Create the output directory structure""" 

352 # Create main directories 

353 dirs = [ 

354 self.output_dir, 

355 self.output_dir / "components", 

356 self.output_dir / "components" / "tools", 

357 self.output_dir / "components" / "resources", 

358 self.output_dir / "components" / "prompts", 

359 ] 

360 

361 for directory in dirs: 

362 directory.mkdir(parents=True, exist_ok=True) 

363 # Process shared files directly in the components directory 

364 self._process_shared_files() 

365 

366 def _process_shared_files(self) -> None: 

367 """Process and transform shared Python files in the components directory 

368 structure.""" 

369 # Process all shared files 

370 for module_path_str, shared_file in self.shared_files.items(): 

371 # Convert module path to Path object (e.g., "tools/weather/helpers") 

372 module_path = Path(module_path_str) 

373 

374 # Determine the component type 

375 component_type = None 

376 for part in module_path.parts: 

377 if part in ["tools", "resources", "prompts"]: 

378 component_type = part 

379 break 

380 

381 if not component_type: 

382 continue 

383 

384 # Calculate target directory in components structure 

385 rel_to_component = module_path.relative_to(component_type) 

386 target_dir = self.output_dir / "components" / component_type / rel_to_component.parent 

387 

388 # Create directory if it doesn't exist 

389 target_dir.mkdir(parents=True, exist_ok=True) 

390 

391 # Create the shared file in the target directory (preserve original filename) 

392 target_file = target_dir / shared_file.name 

393 

394 # Use transformer to process the file 

395 transform_component( 

396 component=None, 

397 output_file=target_file, 

398 project_path=self.project_path, 

399 import_map=self.import_map, 

400 source_file=shared_file, 

401 ) 

402 

403 def _generate_tools(self) -> None: 

404 """Generate code for all tools.""" 

405 tools_dir = self.output_dir / "components" / "tools" 

406 

407 for tool in self.components.get(ComponentType.TOOL, []): 

408 # Get the tool directory structure 

409 rel_path = Path(tool.file_path).relative_to(self.project_path) 

410 if not rel_path.is_relative_to(Path(self.settings.tools_dir)): 

411 console.print(f"[yellow]Warning: Tool {tool.name} is not in the tools directory[/yellow]") 

412 continue 

413 

414 try: 

415 rel_to_tools = rel_path.relative_to(self.settings.tools_dir) 

416 tool_dir = tools_dir / rel_to_tools.parent 

417 except ValueError: 

418 # Fall back to just using the filename 

419 tool_dir = tools_dir 

420 

421 tool_dir.mkdir(parents=True, exist_ok=True) 

422 

423 # Create the tool file 

424 output_file = tool_dir / rel_path.name 

425 transform_component(tool, output_file, self.project_path, self.import_map) 

426 

427 def _generate_resources(self) -> None: 

428 """Generate code for all resources.""" 

429 resources_dir = self.output_dir / "components" / "resources" 

430 

431 for resource in self.components.get(ComponentType.RESOURCE, []): 

432 # Get the resource directory structure 

433 rel_path = Path(resource.file_path).relative_to(self.project_path) 

434 if not rel_path.is_relative_to(Path(self.settings.resources_dir)): 

435 console.print(f"[yellow]Warning: Resource {resource.name} is not in the resources directory[/yellow]") 

436 continue 

437 

438 try: 

439 rel_to_resources = rel_path.relative_to(self.settings.resources_dir) 

440 resource_dir = resources_dir / rel_to_resources.parent 

441 except ValueError: 

442 # Fall back to just using the filename 

443 resource_dir = resources_dir 

444 

445 resource_dir.mkdir(parents=True, exist_ok=True) 

446 

447 # Create the resource file 

448 output_file = resource_dir / rel_path.name 

449 transform_component(resource, output_file, self.project_path, self.import_map) 

450 

451 def _generate_prompts(self) -> None: 

452 """Generate code for all prompts.""" 

453 prompts_dir = self.output_dir / "components" / "prompts" 

454 

455 for prompt in self.components.get(ComponentType.PROMPT, []): 

456 # Get the prompt directory structure 

457 rel_path = Path(prompt.file_path).relative_to(self.project_path) 

458 if not rel_path.is_relative_to(Path(self.settings.prompts_dir)): 

459 console.print(f"[yellow]Warning: Prompt {prompt.name} is not in the prompts directory[/yellow]") 

460 continue 

461 

462 try: 

463 rel_to_prompts = rel_path.relative_to(self.settings.prompts_dir) 

464 prompt_dir = prompts_dir / rel_to_prompts.parent 

465 except ValueError: 

466 # Fall back to just using the filename 

467 prompt_dir = prompts_dir 

468 

469 prompt_dir.mkdir(parents=True, exist_ok=True) 

470 

471 # Create the prompt file 

472 output_file = prompt_dir / rel_path.name 

473 transform_component(prompt, output_file, self.project_path, self.import_map) 

474 

475 def _get_transport_config(self, transport_type: str) -> dict: 

476 """Get transport-specific configuration (primarily for endpoint path display). 

477 

478 Args: 

479 transport_type: The transport type (e.g., 'sse', 'streamable-http', 'stdio') 

480 

481 Returns: 

482 Dictionary with transport configuration details (endpoint_path) 

483 """ 

484 config = { 

485 "endpoint_path": "", 

486 } 

487 

488 if transport_type == "sse": 

489 config["endpoint_path"] = "/sse" # Default SSE path for FastMCP 

490 elif transport_type == "stdio": 

491 config["endpoint_path"] = "" # No HTTP endpoint 

492 else: 

493 # Default to streamable-http 

494 config["endpoint_path"] = "/mcp/" # Default MCP path for FastMCP 

495 

496 return config 

497 

498 def _generate_server(self) -> None: 

499 """Generate the main server entry point.""" 

500 server_file = self.output_dir / "server.py" 

501 

502 # Get auth components 

503 auth_components = generate_auth_code( 

504 server_name=self.settings.name, 

505 host=self.settings.host, 

506 port=self.settings.port, 

507 https=False, # This could be configurable in settings 

508 opentelemetry_enabled=self.settings.opentelemetry_enabled, 

509 transport=self.settings.transport, 

510 ) 

511 

512 # Create imports section 

513 imports = [ 

514 "from fastmcp import FastMCP", 

515 "from fastmcp.tools import Tool", 

516 "from fastmcp.resources import Resource", 

517 "from fastmcp.prompts import Prompt", 

518 "import os", 

519 "import sys", 

520 "from dotenv import load_dotenv", 

521 "import logging", 

522 "", 

523 "# Suppress FastMCP INFO logs", 

524 "logging.getLogger('FastMCP').setLevel(logging.ERROR)", 

525 "logging.getLogger('mcp').setLevel(logging.ERROR)", 

526 "", 

527 "# Golf utilities for MCP features (available for tool functions)", 

528 "# from golf.utilities import elicit, sample, get_current_context", 

529 "", 

530 ] 

531 

532 # Add auth imports if auth is configured 

533 if auth_components.get("has_auth"): 

534 imports.extend(auth_components["imports"]) 

535 imports.append("") 

536 

537 # Add OpenTelemetry imports if enabled 

538 if self.settings.opentelemetry_enabled: 

539 imports.extend(generate_telemetry_imports()) 

540 

541 # Add metrics imports if enabled 

542 if self.settings.metrics_enabled: 

543 from golf.core.builder_metrics import ( 

544 generate_metrics_imports, 

545 generate_metrics_instrumentation, 

546 generate_session_tracking, 

547 ) 

548 

549 imports.extend(generate_metrics_imports()) 

550 imports.extend(generate_metrics_instrumentation()) 

551 imports.extend(generate_session_tracking()) 

552 

553 # Add health check imports if enabled 

554 if self.settings.health_check_enabled: 

555 imports.extend( 

556 [ 

557 "from starlette.requests import Request", 

558 "from starlette.responses import PlainTextResponse", 

559 ] 

560 ) 

561 

562 # Get transport-specific configuration 

563 transport_config = self._get_transport_config(self.settings.transport) 

564 endpoint_path = transport_config["endpoint_path"] 

565 

566 # Track component modules to register 

567 component_registrations = [] 

568 

569 # Import components 

570 for component_type in self.components: 

571 # Add a section header 

572 if component_type == ComponentType.TOOL: 

573 imports.append("# Import tools") 

574 comp_section = "# Register tools" 

575 elif component_type == ComponentType.RESOURCE: 

576 imports.append("# Import resources") 

577 comp_section = "# Register resources" 

578 else: 

579 imports.append("# Import prompts") 

580 comp_section = "# Register prompts" 

581 

582 component_registrations.append(comp_section) 

583 

584 for component in self.components[component_type]: 

585 # Derive the import path based on component type and file path 

586 rel_path = Path(component.file_path).relative_to(self.project_path) 

587 module_name = rel_path.stem 

588 

589 if component_type == ComponentType.TOOL: 

590 try: 

591 rel_to_tools = rel_path.relative_to(self.settings.tools_dir) 

592 # Handle nested directories properly 

593 if rel_to_tools.parent != Path("."): 

594 parent_path = str(rel_to_tools.parent).replace("\\", ".").replace("/", ".") 

595 import_path = f"components.tools.{parent_path}" 

596 else: 

597 import_path = "components.tools" 

598 except ValueError: 

599 import_path = "components.tools" 

600 elif component_type == ComponentType.RESOURCE: 

601 try: 

602 rel_to_resources = rel_path.relative_to(self.settings.resources_dir) 

603 # Handle nested directories properly 

604 if rel_to_resources.parent != Path("."): 

605 parent_path = str(rel_to_resources.parent).replace("\\", ".").replace("/", ".") 

606 import_path = f"components.resources.{parent_path}" 

607 else: 

608 import_path = "components.resources" 

609 except ValueError: 

610 import_path = "components.resources" 

611 else: # PROMPT 

612 try: 

613 rel_to_prompts = rel_path.relative_to(self.settings.prompts_dir) 

614 # Handle nested directories properly 

615 if rel_to_prompts.parent != Path("."): 

616 parent_path = str(rel_to_prompts.parent).replace("\\", ".").replace("/", ".") 

617 import_path = f"components.prompts.{parent_path}" 

618 else: 

619 import_path = "components.prompts" 

620 except ValueError: 

621 import_path = "components.prompts" 

622 

623 # Clean up the import path 

624 import_path = import_path.rstrip(".") 

625 

626 # Add the import for the component's module 

627 full_module_path = f"{import_path}.{module_name}" 

628 imports.append(f"import {full_module_path}") 

629 

630 # Add code to register this component 

631 if self.settings.opentelemetry_enabled: 

632 # Use telemetry instrumentation 

633 registration = f"# Register the {component_type.value} '{component.name}' with telemetry" 

634 entry_func = ( 

635 component.entry_function 

636 if hasattr(component, "entry_function") and component.entry_function 

637 else "export" 

638 ) 

639 

640 registration += ( 

641 f"\n_wrapped_func = instrument_{component_type.value}(" 

642 f"{full_module_path}.{entry_func}, '{component.name}')" 

643 ) 

644 

645 if component_type == ComponentType.TOOL: 

646 registration += ( 

647 f"\n_tool = Tool.from_function(_wrapped_func, " 

648 f'name="{component.name}", ' 

649 f'description="{component.docstring or ""}")' 

650 ) 

651 # Add annotations if present 

652 if hasattr(component, "annotations") and component.annotations: 

653 registration += f".with_annotations({component.annotations})" 

654 registration += "\nmcp.add_tool(_tool)" 

655 elif component_type == ComponentType.RESOURCE: 

656 registration += ( 

657 f"\n_resource = Resource.from_function(_wrapped_func, " 

658 f'uri="{component.uri_template}", name="{component.name}", ' 

659 f'description="{component.docstring or ""}")\n' 

660 f"mcp.add_resource(_resource)" 

661 ) 

662 else: # PROMPT 

663 registration += ( 

664 f"\n_prompt = Prompt.from_function(_wrapped_func, " 

665 f'name="{component.name}", ' 

666 f'description="{component.docstring or ""}")\n' 

667 f"mcp.add_prompt(_prompt)" 

668 ) 

669 elif self.settings.metrics_enabled: 

670 # Use metrics instrumentation 

671 registration = f"# Register the {component_type.value} '{component.name}' with metrics" 

672 entry_func = ( 

673 component.entry_function 

674 if hasattr(component, "entry_function") and component.entry_function 

675 else "export" 

676 ) 

677 

678 registration += ( 

679 f"\n_wrapped_func = instrument_{component_type.value}(" 

680 f"{full_module_path}.{entry_func}, '{component.name}')" 

681 ) 

682 

683 if component_type == ComponentType.TOOL: 

684 registration += ( 

685 f"\n_tool = Tool.from_function(_wrapped_func, " 

686 f'name="{component.name}", ' 

687 f'description="{component.docstring or ""}")' 

688 ) 

689 # Add annotations if present 

690 if hasattr(component, "annotations") and component.annotations: 

691 registration += f".with_annotations({component.annotations})" 

692 registration += "\nmcp.add_tool(_tool)" 

693 elif component_type == ComponentType.RESOURCE: 

694 registration += ( 

695 f"\n_resource = Resource.from_function(_wrapped_func, " 

696 f'uri="{component.uri_template}", name="{component.name}", ' 

697 f'description="{component.docstring or ""}")\n' 

698 f"mcp.add_resource(_resource)" 

699 ) 

700 else: # PROMPT 

701 registration += ( 

702 f"\n_prompt = Prompt.from_function(_wrapped_func, " 

703 f'name="{component.name}", ' 

704 f'description="{component.docstring or ""}")\n' 

705 f"mcp.add_prompt(_prompt)" 

706 ) 

707 else: 

708 # Standard registration without telemetry 

709 if component_type == ComponentType.TOOL: 

710 registration = f"# Register the tool '{component.name}' from {full_module_path}" 

711 

712 # Use the entry_function if available, otherwise try the 

713 # export variable 

714 if hasattr(component, "entry_function") and component.entry_function: 

715 registration += ( 

716 f"\n_tool = Tool.from_function({full_module_path}.{component.entry_function}" 

717 ) 

718 else: 

719 registration += f"\n_tool = Tool.from_function({full_module_path}.export" 

720 

721 # Add the name parameter 

722 registration += f', name="{component.name}"' 

723 

724 # Add description from docstring 

725 if component.docstring: 

726 # Escape any quotes in the docstring 

727 escaped_docstring = component.docstring.replace('"', '\\"') 

728 registration += f', description="{escaped_docstring}"' 

729 

730 registration += ")" 

731 

732 # Add annotations if present 

733 if hasattr(component, "annotations") and component.annotations: 

734 registration += f"\n_tool = _tool.with_annotations({component.annotations})" 

735 

736 registration += "\nmcp.add_tool(_tool)" 

737 

738 elif component_type == ComponentType.RESOURCE: 

739 registration = f"# Register the resource '{component.name}' from {full_module_path}" 

740 

741 # Use the entry_function if available, otherwise try the 

742 # export variable 

743 if hasattr(component, "entry_function") and component.entry_function: 

744 registration += ( 

745 f"\n_resource = Resource.from_function(" 

746 f"{full_module_path}.{component.entry_function}, " 

747 f'uri="{component.uri_template}"' 

748 ) 

749 else: 

750 registration += ( 

751 f"\n_resource = Resource.from_function(" 

752 f"{full_module_path}.export, " 

753 f'uri="{component.uri_template}"' 

754 ) 

755 

756 # Add the name parameter 

757 registration += f', name="{component.name}"' 

758 

759 # Add description from docstring 

760 if component.docstring: 

761 # Escape any quotes in the docstring 

762 escaped_docstring = component.docstring.replace('"', '\\"') 

763 registration += f', description="{escaped_docstring}"' 

764 

765 registration += ")\nmcp.add_resource(_resource)" 

766 

767 else: # PROMPT 

768 registration = f"# Register the prompt '{component.name}' from {full_module_path}" 

769 

770 # Use the entry_function if available, otherwise try the 

771 # export variable 

772 if hasattr(component, "entry_function") and component.entry_function: 

773 registration += ( 

774 f"\n_prompt = Prompt.from_function({full_module_path}.{component.entry_function}" 

775 ) 

776 else: 

777 registration += f"\n_prompt = Prompt.from_function({full_module_path}.export" 

778 

779 # Add the name parameter 

780 registration += f', name="{component.name}"' 

781 

782 # Add description from docstring 

783 if component.docstring: 

784 # Escape any quotes in the docstring 

785 escaped_docstring = component.docstring.replace('"', '\\"') 

786 registration += f', description="{escaped_docstring}"' 

787 

788 registration += ")\nmcp.add_prompt(_prompt)" 

789 

790 component_registrations.append(registration) 

791 

792 # Add a blank line after each section 

793 imports.append("") 

794 component_registrations.append("") 

795 

796 # Create environment section based on build type - moved after imports 

797 env_section = [ 

798 "", 

799 "# Load environment variables from .env file if it exists", 

800 "# Note: dotenv will not override existing environment variables by default", 

801 "load_dotenv()", 

802 "", 

803 ] 

804 

805 # OpenTelemetry setup code will be handled through imports and lifespan 

806 

807 # Add auth setup code if auth is configured 

808 auth_setup_code = [] 

809 if auth_components.get("has_auth"): 

810 auth_setup_code = auth_components["setup_code"] 

811 

812 # Create FastMCP instance section 

813 server_code_lines = ["# Create FastMCP server"] 

814 

815 # Build FastMCP constructor arguments 

816 mcp_constructor_args = [f'"{self.settings.name}"'] 

817 

818 # Add auth arguments if configured 

819 if auth_components.get("has_auth") and auth_components.get("fastmcp_args"): 

820 for key, value in auth_components["fastmcp_args"].items(): 

821 mcp_constructor_args.append(f"{key}={value}") 

822 

823 # Add stateless HTTP parameter if enabled 

824 if self.settings.stateless_http: 

825 mcp_constructor_args.append("stateless_http=True") 

826 

827 # Add OpenTelemetry parameters if enabled 

828 if self.settings.opentelemetry_enabled: 

829 mcp_constructor_args.append("lifespan=telemetry_lifespan") 

830 

831 mcp_instance_line = f"mcp = FastMCP({', '.join(mcp_constructor_args)})" 

832 server_code_lines.append(mcp_instance_line) 

833 server_code_lines.append("") 

834 

835 # Add early telemetry initialization if enabled (before component registration) 

836 early_telemetry_init = [] 

837 if self.settings.opentelemetry_enabled: 

838 early_telemetry_init.extend( 

839 [ 

840 "# Initialize telemetry early to ensure instrumentation works", 

841 "from golf.telemetry.instrumentation import init_telemetry, set_detailed_tracing", 

842 f'init_telemetry("{self.settings.name}")', 

843 f"set_detailed_tracing({self.settings.detailed_tracing})", 

844 "", 

845 ] 

846 ) 

847 

848 # Add metrics initialization if enabled 

849 early_metrics_init = [] 

850 if self.settings.metrics_enabled: 

851 from golf.core.builder_metrics import generate_metrics_initialization 

852 

853 early_metrics_init.extend(generate_metrics_initialization(self.settings.name)) 

854 

855 # Main entry point with transport-specific app initialization 

856 main_code = [ 

857 'if __name__ == "__main__":', 

858 " from rich.console import Console", 

859 " from rich.panel import Panel", 

860 " console = Console()", 

861 " # Get configuration from environment variables or use defaults", 

862 ' host = os.environ.get("HOST", "localhost")', 

863 ' port = int(os.environ.get("PORT", 3000))', 

864 f' transport_to_run = "{self.settings.transport}"', 

865 "", 

866 ] 

867 

868 main_code.append("") 

869 

870 # Transport-specific run methods 

871 if self.settings.transport == "sse": 

872 # Check if we need middleware for SSE 

873 middleware_setup = [] 

874 middleware_list = [] 

875 

876 api_key_config = get_api_key_config() 

877 if auth_components.get("has_auth") and api_key_config: 

878 middleware_setup.append(" from starlette.middleware import Middleware") 

879 middleware_list.append("Middleware(ApiKeyMiddleware)") 

880 

881 # Add metrics middleware if enabled 

882 if self.settings.metrics_enabled: 

883 middleware_setup.append(" from starlette.middleware import Middleware") 

884 middleware_list.append("Middleware(MetricsMiddleware)") 

885 

886 # Add OpenTelemetry middleware if enabled 

887 if self.settings.opentelemetry_enabled: 

888 middleware_setup.append(" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware") 

889 middleware_setup.append(" from starlette.middleware import Middleware") 

890 middleware_list.append("Middleware(OpenTelemetryMiddleware)") 

891 

892 if middleware_setup: 

893 main_code.extend(middleware_setup) 

894 main_code.append(f" middleware = [{', '.join(middleware_list)}]") 

895 main_code.append("") 

896 main_code.extend( 

897 [ 

898 " # Run SSE server with middleware using FastMCP's run method", 

899 f' mcp.run(transport="sse", host=host, port=port, ' 

900 f'path="{endpoint_path}", log_level="info", ' 

901 f"middleware=middleware, show_banner=False)", 

902 ] 

903 ) 

904 else: 

905 main_code.extend( 

906 [ 

907 " # Run SSE server using FastMCP's run method", 

908 f' mcp.run(transport="sse", host=host, port=port, ' 

909 f'path="{endpoint_path}", log_level="info", ' 

910 f"show_banner=False)", 

911 ] 

912 ) 

913 

914 elif self.settings.transport in ["streamable-http", "http"]: 

915 # Check if we need middleware for streamable-http 

916 middleware_setup = [] 

917 middleware_list = [] 

918 

919 api_key_config = get_api_key_config() 

920 if auth_components.get("has_auth") and api_key_config: 

921 middleware_setup.append(" from starlette.middleware import Middleware") 

922 middleware_list.append("Middleware(ApiKeyMiddleware)") 

923 

924 # Add metrics middleware if enabled 

925 if self.settings.metrics_enabled: 

926 middleware_setup.append(" from starlette.middleware import Middleware") 

927 middleware_list.append("Middleware(MetricsMiddleware)") 

928 

929 # Add OpenTelemetry middleware if enabled 

930 if self.settings.opentelemetry_enabled: 

931 middleware_setup.append(" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware") 

932 middleware_setup.append(" from starlette.middleware import Middleware") 

933 middleware_list.append("Middleware(OpenTelemetryMiddleware)") 

934 

935 if middleware_setup: 

936 main_code.extend(middleware_setup) 

937 main_code.append(f" middleware = [{', '.join(middleware_list)}]") 

938 main_code.append("") 

939 main_code.extend( 

940 [ 

941 " # Run HTTP server with middleware using FastMCP's run method", 

942 f' mcp.run(transport="streamable-http", host=host, ' 

943 f'port=port, path="{endpoint_path}", log_level="info", ' 

944 f"middleware=middleware, show_banner=False)", 

945 ] 

946 ) 

947 else: 

948 main_code.extend( 

949 [ 

950 " # Run HTTP server using FastMCP's run method", 

951 f' mcp.run(transport="streamable-http", host=host, ' 

952 f'port=port, path="{endpoint_path}", log_level="info", ' 

953 f"show_banner=False)", 

954 ] 

955 ) 

956 else: 

957 # For stdio transport, use mcp.run() 

958 main_code.extend([" # Run with stdio transport", ' mcp.run(transport="stdio", show_banner=False)']) 

959 

960 # Add metrics route if enabled 

961 metrics_route_code = [] 

962 if self.settings.metrics_enabled: 

963 from golf.core.builder_metrics import generate_metrics_route 

964 

965 metrics_route_code = generate_metrics_route(self.settings.metrics_path) 

966 

967 # Add health check route if enabled 

968 health_check_code = [] 

969 if self.settings.health_check_enabled: 

970 health_check_code = [ 

971 "# Add health check route", 

972 "@mcp.custom_route('" + self.settings.health_check_path + '\', methods=["GET"])', 

973 "async def health_check(request: Request) -> PlainTextResponse:", 

974 ' """Health check endpoint for Kubernetes and load balancers."""', 

975 (f' return PlainTextResponse("{self.settings.health_check_response}")'), 

976 "", 

977 ] 

978 

979 # Combine all sections 

980 # Order: imports, env_section, auth_setup, server_code (mcp init), 

981 # early_telemetry_init, early_metrics_init, component_registrations, 

982 # metrics_route_code, health_check_code, main_code (run block) 

983 code = "\n".join( 

984 imports 

985 + env_section 

986 + auth_setup_code 

987 + server_code_lines 

988 + early_telemetry_init 

989 + early_metrics_init 

990 + component_registrations 

991 + metrics_route_code 

992 + health_check_code 

993 + main_code 

994 ) 

995 

996 # Format with black 

997 try: 

998 code = black.format_str(code, mode=black.Mode()) 

999 except Exception as e: 

1000 console.print(f"[yellow]Warning: Could not format server.py: {e}[/yellow]") 

1001 

1002 # Write to file 

1003 with open(server_file, "w") as f: 

1004 f.write(code) 

1005 

1006 

1007def build_project( 

1008 project_path: Path, 

1009 settings: Settings, 

1010 output_dir: Path, 

1011 build_env: str = "prod", 

1012 copy_env: bool = False, 

1013) -> None: 

1014 """Build a standalone FastMCP application from a GolfMCP project. 

1015 

1016 Args: 

1017 project_path: Path to the project directory 

1018 settings: Project settings 

1019 output_dir: Output directory for the built application 

1020 build_env: Build environment ('dev' or 'prod') 

1021 copy_env: Whether to copy environment variables to the built app 

1022 """ 

1023 # Load Golf credentials from .env for build operations (platform registration, etc.) 

1024 # This happens regardless of copy_env setting to ensure build process works 

1025 from dotenv import load_dotenv 

1026 

1027 project_env_file = project_path / ".env" 

1028 if project_env_file.exists(): 

1029 # Load GOLF_* variables for build process 

1030 load_dotenv(project_env_file, override=False) 

1031 

1032 # Only log if we actually found the specific Golf platform credentials 

1033 has_api_key = "GOLF_API_KEY" in os.environ 

1034 has_server_id = "GOLF_SERVER_ID" in os.environ 

1035 if has_api_key and has_server_id: 

1036 console.print("[dim]Loaded Golf credentials for build operations[/dim]") 

1037 

1038 # Execute auth.py if it exists (for authentication configuration) 

1039 # Also support legacy pre_build.py for backward compatibility 

1040 auth_path = project_path / "auth.py" 

1041 legacy_path = project_path / "pre_build.py" 

1042 

1043 config_path = None 

1044 if auth_path.exists(): 

1045 config_path = auth_path 

1046 elif legacy_path.exists(): 

1047 config_path = legacy_path 

1048 console.print("[yellow]Warning: pre_build.py is deprecated. Rename to auth.py[/yellow]") 

1049 

1050 if config_path: 

1051 # Save the current directory and path - handle case where cwd might be invalid 

1052 try: 

1053 original_dir = os.getcwd() 

1054 except (FileNotFoundError, OSError): 

1055 # Current directory might have been deleted by previous operations, 

1056 # use project_path as fallback 

1057 original_dir = str(project_path) 

1058 os.chdir(original_dir) 

1059 original_path = sys.path.copy() 

1060 

1061 try: 

1062 # Change to the project directory and add it to Python path 

1063 os.chdir(project_path) 

1064 sys.path.insert(0, str(project_path)) 

1065 

1066 # Execute the auth configuration script 

1067 with open(config_path) as f: 

1068 script_content = f.read() 

1069 

1070 # Print the first few lines for debugging 

1071 "\n".join(script_content.split("\n")[:5]) + "\n..." 

1072 

1073 # Use exec to run the script as a module 

1074 code = compile(script_content, str(config_path), "exec") 

1075 exec(code, {}) 

1076 

1077 except Exception as e: 

1078 console.print(f"[red]Error executing {config_path.name}: {str(e)}[/red]") 

1079 import traceback 

1080 

1081 console.print(f"[red]{traceback.format_exc()}[/red]") 

1082 

1083 # Track detailed error for auth.py execution failures 

1084 try: 

1085 from golf.core.telemetry import track_detailed_error 

1086 

1087 track_detailed_error( 

1088 "build_auth_failed", 

1089 e, 

1090 context=f"Executing {config_path.name} configuration script", 

1091 operation="auth_execution", 

1092 additional_props={ 

1093 "file_path": str(config_path.relative_to(project_path)), 

1094 "build_env": build_env, 

1095 }, 

1096 ) 

1097 except Exception: 

1098 # Don't let telemetry errors break the build 

1099 pass 

1100 finally: 

1101 # Always restore original directory and path, even if an exception occurred 

1102 try: 

1103 os.chdir(original_dir) 

1104 sys.path = original_path 

1105 except Exception: 

1106 # If we can't restore the directory, at least try to reset the path 

1107 sys.path = original_path 

1108 

1109 # Clear the output directory if it exists 

1110 if output_dir.exists(): 

1111 shutil.rmtree(output_dir) 

1112 output_dir.mkdir(parents=True, exist_ok=True) # Ensure output_dir exists after clearing 

1113 

1114 # --- BEGIN Enhanced .env handling --- 

1115 env_vars_to_write = {} 

1116 env_file_path = output_dir / ".env" 

1117 

1118 # 1. Load from existing project .env if copy_env is true 

1119 if copy_env: 

1120 project_env_file = project_path / ".env" 

1121 if project_env_file.exists(): 

1122 try: 

1123 from dotenv import dotenv_values 

1124 

1125 env_vars_to_write.update(dotenv_values(project_env_file)) 

1126 except ImportError: 

1127 console.print( 

1128 "[yellow]Warning: python-dotenv is not installed. " 

1129 "Cannot read existing .env file for rich merging. " 

1130 "Copying directly.[/yellow]" 

1131 ) 

1132 try: 

1133 shutil.copy(project_env_file, env_file_path) 

1134 # If direct copy happens, re-read for step 2 & 3 to respect 

1135 # its content 

1136 if env_file_path.exists(): 

1137 from dotenv import dotenv_values 

1138 

1139 env_vars_to_write.update(dotenv_values(env_file_path)) # Read what was copied 

1140 except Exception as e: 

1141 console.print(f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]") 

1142 except Exception as e: 

1143 console.print(f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]") 

1144 

1145 # 2. Apply Golf's OTel default exporter setting if OTEL_TRACES_EXPORTER 

1146 # is not already set 

1147 if ( 

1148 settings.opentelemetry_enabled 

1149 and settings.opentelemetry_default_exporter 

1150 and "OTEL_TRACES_EXPORTER" not in env_vars_to_write 

1151 ): 

1152 env_vars_to_write["OTEL_TRACES_EXPORTER"] = settings.opentelemetry_default_exporter 

1153 

1154 # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set 

1155 # (Ensures service name defaults to project name if not specified in user's .env) 

1156 if settings.opentelemetry_enabled and settings.name and "OTEL_SERVICE_NAME" not in env_vars_to_write: 

1157 env_vars_to_write["OTEL_SERVICE_NAME"] = settings.name 

1158 

1159 # 4. (Re-)Write the .env file in the output directory if there's anything to write 

1160 if env_vars_to_write: 

1161 try: 

1162 with open(env_file_path, "w") as f: 

1163 for key, value in env_vars_to_write.items(): 

1164 # Ensure values are properly quoted if they contain spaces or special characters 

1165 # and handle existing quotes within the value. 

1166 if isinstance(value, str): 

1167 # Replace backslashes first, then double quotes 

1168 processed_value = value.replace("\\", "\\\\") # Escape backslashes 

1169 processed_value = processed_value.replace('"', '\\"') # Escape double quotes 

1170 if " " in value or "#" in value or "\n" in value or '"' in value or "'" in value: 

1171 f.write(f'{key}="{processed_value}"\n') 

1172 else: 

1173 f.write(f"{key}={processed_value}\n") 

1174 else: # For non-string values, write directly 

1175 f.write(f"{key}={value}\n") 

1176 except Exception as e: 

1177 console.print(f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]") 

1178 # --- END Enhanced .env handling --- 

1179 

1180 # Show what we're building, with environment info 

1181 create_build_header(settings.name, build_env, console) 

1182 

1183 # Generate the code 

1184 generator = CodeGenerator(project_path, settings, output_dir, build_env=build_env, copy_env=copy_env) 

1185 generator.generate() 

1186 

1187 # Platform registration (only for prod builds) 

1188 if build_env == "prod": 

1189 console.print() 

1190 status_msg = f"[{GOLF_BLUE}]{STATUS_ICONS['platform']} Registering with Golf platform and updating resources...[/{GOLF_BLUE}]" 

1191 with console.status(status_msg): 

1192 import asyncio 

1193 

1194 try: 

1195 from golf.core.platform import register_project_with_platform 

1196 

1197 success = asyncio.run( 

1198 register_project_with_platform( 

1199 project_path=project_path, 

1200 settings=settings, 

1201 components=generator.components, 

1202 ) 

1203 ) 

1204 

1205 if success: 

1206 console.print(get_status_text("success", "Platform registration completed")) 

1207 # If success is False, the platform module already printed appropriate warnings 

1208 except ImportError: 

1209 console.print(get_status_text("warning", "Platform registration module not available")) 

1210 except Exception as e: 

1211 console.print(get_status_text("warning", f"Platform registration failed: {e}")) 

1212 console.print("[dim]Tip: Ensure GOLF_API_KEY and GOLF_SERVER_ID are available in your .env file[/dim]") 

1213 

1214 # Create a simple README 

1215 readme_content = f"""# {settings.name} 

1216 

1217Generated FastMCP application ({build_env} environment). 

1218 

1219## Running the server 

1220 

1221```bash 

1222cd {output_dir.name} 

1223python server.py 

1224``` 

1225 

1226This is a standalone FastMCP server generated by GolfMCP. 

1227""" 

1228 

1229 with open(output_dir / "README.md", "w") as f: 

1230 f.write(readme_content) 

1231 

1232 # Always copy the auth module so it's available 

1233 auth_dir = output_dir / "golf" / "auth" 

1234 auth_dir.mkdir(parents=True, exist_ok=True) 

1235 

1236 # Create __init__.py with needed exports 

1237 with open(auth_dir / "__init__.py", "w") as f: 

1238 f.write( 

1239 """\"\"\"Auth module for GolfMCP.\"\"\" 

1240 

1241# Legacy ProviderConfig removed in Golf 0.2.x - use modern auth configurations 

1242# Legacy OAuth imports removed in Golf 0.2.x - use FastMCP 2.11+ auth providers 

1243from golf.auth.helpers import get_provider_token, extract_token_from_header, get_api_key, set_api_key 

1244from golf.auth.api_key import configure_api_key, get_api_key_config 

1245from golf.auth.factory import create_auth_provider 

1246from golf.auth.providers import RemoteAuthConfig, JWTAuthConfig, StaticTokenConfig, OAuthServerConfig 

1247""" 

1248 ) 

1249 

1250 # Copy auth modules required for Golf 0.2.x 

1251 for module in ["helpers.py", "api_key.py", "factory.py", "providers.py"]: 

1252 src_file = Path(__file__).parent.parent.parent / "golf" / "auth" / module 

1253 dst_file = auth_dir / module 

1254 

1255 if src_file.exists(): 

1256 shutil.copy(src_file, dst_file) 

1257 else: 

1258 console.print(f"[yellow]Warning: Could not find {src_file} to copy[/yellow]") 

1259 

1260 # Copy telemetry module if OpenTelemetry is enabled 

1261 if settings.opentelemetry_enabled: 

1262 telemetry_dir = output_dir / "golf" / "telemetry" 

1263 telemetry_dir.mkdir(parents=True, exist_ok=True) 

1264 

1265 # Copy telemetry __init__.py 

1266 src_init = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py" 

1267 dst_init = telemetry_dir / "__init__.py" 

1268 if src_init.exists(): 

1269 shutil.copy(src_init, dst_init) 

1270 

1271 # Copy instrumentation module 

1272 src_instrumentation = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "instrumentation.py" 

1273 dst_instrumentation = telemetry_dir / "instrumentation.py" 

1274 if src_instrumentation.exists(): 

1275 shutil.copy(src_instrumentation, dst_instrumentation) 

1276 else: 

1277 console.print("[yellow]Warning: Could not find telemetry instrumentation module[/yellow]") 

1278 

1279 # Check if auth routes need to be added 

1280 if is_auth_configured() or get_api_key_config(): 

1281 auth_routes_code = generate_auth_routes() 

1282 

1283 server_file = output_dir / "server.py" 

1284 if server_file.exists(): 

1285 with open(server_file) as f: 

1286 server_code_content = f.read() 

1287 

1288 # Add auth routes before the main block 

1289 app_marker = 'if __name__ == "__main__":' 

1290 app_pos = server_code_content.find(app_marker) 

1291 if app_pos != -1: 

1292 modified_code = ( 

1293 server_code_content[:app_pos] + auth_routes_code + "\n\n" + server_code_content[app_pos:] 

1294 ) 

1295 

1296 # Format with black before writing 

1297 try: 

1298 final_code_to_write = black.format_str(modified_code, mode=black.Mode()) 

1299 except Exception as e: 

1300 console.print( 

1301 f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]" 

1302 ) 

1303 final_code_to_write = modified_code 

1304 

1305 with open(server_file, "w") as f: 

1306 f.write(final_code_to_write) 

1307 else: 

1308 console.print( 

1309 f"[yellow]Warning: Could not find main block marker '{app_marker}' in {server_file} to inject auth routes.[/yellow]" 

1310 ) 

1311 

1312 

1313# Legacy function removed - replaced by parse_shared_files in parser module 

1314 

1315 

1316# Updated to handle any shared file, not just common.py files 

1317def build_import_map(project_path: Path, shared_files: dict[str, Path]) -> dict[str, str]: 

1318 """Build a mapping of import paths to their new locations in the build output. 

1319 

1320 This maps from original relative import paths to absolute import paths 

1321 in the components directory structure. 

1322  

1323 Args: 

1324 project_path: Path to the project root 

1325 shared_files: Dictionary mapping module paths to shared file paths 

1326 """ 

1327 import_map = {} 

1328 

1329 for module_path_str, file_path in shared_files.items(): 

1330 # Convert module path to Path object (e.g., "tools/weather/helpers" -> Path("tools/weather/helpers")) 

1331 module_path = Path(module_path_str) 

1332 

1333 # Get the component type (tools, resources, prompts) 

1334 component_type = None 

1335 for part in module_path.parts: 

1336 if part in ["tools", "resources", "prompts"]: 

1337 component_type = part 

1338 break 

1339 

1340 if not component_type: 

1341 continue 

1342 

1343 # Calculate the relative path within the component type 

1344 try: 

1345 rel_to_component = module_path.relative_to(component_type) 

1346 # Create the new import path 

1347 if str(rel_to_component) == ".": 

1348 # This shouldn't happen for individual files, but handle it 

1349 new_path = f"components.{component_type}" 

1350 else: 

1351 # Replace path separators with dots 

1352 path_parts = str(rel_to_component).replace("\\", "/").split("/") 

1353 new_path = f"components.{component_type}.{'.'.join(path_parts)}" 

1354 

1355 # Map the specific shared module 

1356 # e.g., "tools/weather/helpers" -> "components.tools.weather.helpers" 

1357 import_map[module_path_str] = new_path 

1358 

1359 # Also map the directory path for relative imports 

1360 # e.g., "tools/weather" -> "components.tools.weather"  

1361 dir_path_str = str(module_path.parent) 

1362 if dir_path_str != "." and dir_path_str not in import_map: 

1363 dir_rel_to_component = module_path.parent.relative_to(component_type) 

1364 if str(dir_rel_to_component) == ".": 

1365 dir_new_path = f"components.{component_type}" 

1366 else: 

1367 dir_path_parts = str(dir_rel_to_component).replace("\\", "/").split("/") 

1368 dir_new_path = f"components.{component_type}.{'.'.join(dir_path_parts)}" 

1369 import_map[dir_path_str] = dir_new_path 

1370 

1371 except ValueError: 

1372 continue 

1373 

1374 return import_map