golf-mcp 0.1.20__py3-none-any.whl → 0.2.0__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.

Files changed (123) hide show
  1. golf/__init__.py +9 -1
  2. golf/_endpoints.py +6 -0
  3. golf/_endpoints_fallback.py +10 -0
  4. golf/auth/__init__.py +188 -84
  5. golf/auth/api_key.py +6 -14
  6. golf/auth/factory.py +333 -0
  7. golf/auth/helpers.py +12 -42
  8. golf/auth/providers.py +396 -0
  9. golf/auth/registry.py +256 -0
  10. golf/cli/branding.py +192 -0
  11. golf/cli/main.py +28 -69
  12. golf/commands/__init__.py +2 -0
  13. golf/commands/build.py +4 -7
  14. golf/commands/init.py +30 -53
  15. golf/commands/run.py +50 -20
  16. golf/core/builder.py +355 -414
  17. golf/core/builder_auth.py +63 -144
  18. golf/core/builder_telemetry.py +26 -3
  19. golf/core/config.py +38 -59
  20. golf/core/parser.py +132 -139
  21. golf/core/platform.py +12 -10
  22. golf/core/telemetry.py +11 -19
  23. golf/core/transformer.py +38 -15
  24. golf/examples/__pycache__/__init__.cpython-311.pyc +0 -0
  25. golf/examples/basic/.coverage +0 -0
  26. golf/examples/basic/.env.example +8 -4
  27. golf/examples/basic/README.md +117 -45
  28. golf/examples/basic/__pycache__/auth.cpython-311.pyc +0 -0
  29. golf/examples/basic/auth.py +76 -0
  30. golf/examples/basic/golf.json +2 -5
  31. golf/examples/basic/htmlcov/.gitignore +2 -0
  32. golf/examples/basic/htmlcov/class_index.html +547 -0
  33. golf/examples/basic/htmlcov/coverage_html_cb_6fb7b396.js +733 -0
  34. golf/examples/basic/htmlcov/favicon_32_cb_58284776.png +0 -0
  35. golf/examples/basic/htmlcov/function_index.html +2091 -0
  36. golf/examples/basic/htmlcov/index.html +349 -0
  37. golf/examples/basic/htmlcov/keybd_closed_cb_ce680311.png +0 -0
  38. golf/examples/basic/htmlcov/status.json +1 -0
  39. golf/examples/basic/htmlcov/style_cb_8e611ae1.css +337 -0
  40. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496___init___py.html +323 -0
  41. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_api_key_py.html +170 -0
  42. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_factory_py.html +430 -0
  43. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_helpers_py.html +288 -0
  44. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_providers_py.html +493 -0
  45. golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_registry_py.html +353 -0
  46. golf/examples/basic/htmlcov/z_3ec3b3f490dc0950___init___py.html +120 -0
  47. golf/examples/basic/htmlcov/z_3ec3b3f490dc0950_instrumentation_py.html +1535 -0
  48. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db___init___py.html +98 -0
  49. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_branding_py.html +289 -0
  50. golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_main_py.html +476 -0
  51. golf/examples/basic/htmlcov/z_5a6c4e6bcc86fb2f___init___py.html +97 -0
  52. golf/examples/basic/htmlcov/z_6cadab9ec0df475d___init___py.html +102 -0
  53. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_build_py.html +178 -0
  54. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_init_py.html +387 -0
  55. golf/examples/basic/htmlcov/z_6cadab9ec0df475d_run_py.html +222 -0
  56. golf/examples/basic/htmlcov/z_6fcdee0582ba84e4___init___py.html +106 -0
  57. golf/examples/basic/htmlcov/z_6fcdee0582ba84e4__endpoints_fallback_py.html +107 -0
  58. golf/examples/basic/htmlcov/z_7ba499ed22986217___init___py.html +98 -0
  59. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_auth_py.html +306 -0
  60. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_metrics_py.html +329 -0
  61. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_py.html +1471 -0
  62. golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_telemetry_py.html +186 -0
  63. golf/examples/basic/htmlcov/z_7ba499ed22986217_config_py.html +315 -0
  64. golf/examples/basic/htmlcov/z_7ba499ed22986217_parser_py.html +1149 -0
  65. golf/examples/basic/htmlcov/z_7ba499ed22986217_platform_py.html +279 -0
  66. golf/examples/basic/htmlcov/z_7ba499ed22986217_telemetry_py.html +589 -0
  67. golf/examples/basic/htmlcov/z_7ba499ed22986217_transformer_py.html +286 -0
  68. golf/examples/basic/htmlcov/z_7d7da37693a43688___init___py.html +107 -0
  69. golf/examples/basic/htmlcov/z_7d7da37693a43688_collector_py.html +417 -0
  70. golf/examples/basic/htmlcov/z_7d7da37693a43688_registry_py.html +109 -0
  71. golf/examples/basic/htmlcov/z_abe733142b40ad4e___init___py.html +109 -0
  72. golf/examples/basic/htmlcov/z_abe733142b40ad4e_context_py.html +150 -0
  73. golf/examples/basic/htmlcov/z_abe733142b40ad4e_elicitation_py.html +267 -0
  74. golf/examples/basic/htmlcov/z_abe733142b40ad4e_sampling_py.html +318 -0
  75. golf/examples/basic/prompts/__pycache__/welcome.cpython-311.pyc +0 -0
  76. golf/examples/basic/prompts/welcome.py +3 -5
  77. golf/examples/basic/resources/__pycache__/current_time.cpython-311.pyc +0 -0
  78. golf/examples/basic/resources/__pycache__/info.cpython-311.pyc +0 -0
  79. golf/examples/basic/resources/current_time.py +5 -13
  80. golf/examples/basic/resources/weather/__pycache__/common.cpython-311.pyc +0 -0
  81. golf/examples/basic/resources/weather/__pycache__/current.cpython-311.pyc +0 -0
  82. golf/examples/basic/resources/weather/__pycache__/forecast.cpython-311.pyc +0 -0
  83. golf/examples/basic/resources/weather/city.py +46 -0
  84. golf/examples/basic/resources/weather/common.py +4 -11
  85. golf/examples/basic/resources/weather/current.py +5 -5
  86. golf/examples/basic/resources/weather/forecast.py +5 -5
  87. golf/examples/basic/tools/__pycache__/calculator.cpython-311.pyc +0 -0
  88. golf/examples/basic/tools/calculator.py +94 -0
  89. golf/examples/basic/tools/say/__pycache__/hello.cpython-311.pyc +0 -0
  90. golf/examples/basic/tools/say/hello.py +65 -0
  91. golf/metrics/collector.py +100 -19
  92. golf/telemetry/__init__.py +4 -0
  93. golf/telemetry/instrumentation.py +484 -178
  94. golf/utilities/__init__.py +12 -0
  95. golf/utilities/context.py +53 -0
  96. golf/utilities/elicitation.py +170 -0
  97. golf/utilities/sampling.py +221 -0
  98. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.0.dist-info}/METADATA +51 -104
  99. golf_mcp-0.2.0.dist-info/RECORD +110 -0
  100. golf/auth/oauth.py +0 -861
  101. golf/auth/provider.py +0 -115
  102. golf/examples/api_key/.env +0 -2
  103. golf/examples/api_key/.env.example +0 -1
  104. golf/examples/api_key/README.md +0 -84
  105. golf/examples/api_key/golf.json +0 -8
  106. golf/examples/api_key/pre_build.py +0 -11
  107. golf/examples/api_key/tools/issues/create.py +0 -93
  108. golf/examples/api_key/tools/issues/list.py +0 -92
  109. golf/examples/api_key/tools/repos/list.py +0 -111
  110. golf/examples/api_key/tools/search/code.py +0 -106
  111. golf/examples/api_key/tools/users/get.py +0 -82
  112. golf/examples/basic/.env +0 -5
  113. golf/examples/basic/pre_build.py +0 -28
  114. golf/examples/basic/tools/github_user.py +0 -65
  115. golf/examples/basic/tools/hello.py +0 -34
  116. golf/examples/basic/tools/payments/charge.py +0 -70
  117. golf/examples/basic/tools/payments/common.py +0 -36
  118. golf/examples/basic/tools/payments/refund.py +0 -61
  119. golf_mcp-0.1.20.dist-info/RECORD +0 -60
  120. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.0.dist-info}/WHEEL +0 -0
  121. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  122. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  123. {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.0.dist-info}/top_level.txt +0 -0
golf/core/builder.py CHANGED
@@ -9,15 +9,14 @@ from typing import Any
9
9
 
10
10
  import black
11
11
  from rich.console import Console
12
- from rich.progress import Progress, SpinnerColumn, TextColumn
13
12
 
14
- from golf.auth import get_auth_config
13
+ from golf.auth import is_auth_configured
15
14
  from golf.auth.api_key import get_api_key_config
16
15
  from golf.core.builder_auth import generate_auth_code, generate_auth_routes
17
16
  from golf.core.builder_telemetry import (
18
17
  generate_telemetry_imports,
19
- get_otel_dependencies,
20
18
  )
19
+ from golf.cli.branding import create_build_header, get_status_text, STATUS_ICONS, GOLF_BLUE
21
20
  from golf.core.config import Settings
22
21
  from golf.core.parser import (
23
22
  ComponentType,
@@ -109,9 +108,7 @@ class ManifestBuilder:
109
108
  """Process all resource components and add them to the manifest."""
110
109
  for component in self.components[ComponentType.RESOURCE]:
111
110
  if not component.uri_template:
112
- console.print(
113
- f"[yellow]Warning: Resource {component.name} has no URI template[/yellow]"
114
- )
111
+ console.print(f"[yellow]Warning: Resource {component.name} has no URI template[/yellow]")
115
112
  continue
116
113
 
117
114
  resource_schema = {
@@ -127,7 +124,8 @@ class ManifestBuilder:
127
124
  def _process_prompts(self) -> None:
128
125
  """Process all prompt components and add them to the manifest."""
129
126
  for component in self.components[ComponentType.PROMPT]:
130
- # For prompts, the handler will have to load the module and execute the run function
127
+ # For prompts, the handler will have to load the module and execute
128
+ # the run function
131
129
  # to get the actual messages, so we just register it by name
132
130
  prompt_schema = {
133
131
  "name": component.name,
@@ -188,9 +186,7 @@ def build_manifest(project_path: Path, settings: Settings) -> dict[str, Any]:
188
186
  return builder.build()
189
187
 
190
188
 
191
- def compute_manifest_diff(
192
- old_manifest: dict[str, Any], new_manifest: dict[str, Any]
193
- ) -> dict[str, Any]:
189
+ def compute_manifest_diff(old_manifest: dict[str, Any], new_manifest: dict[str, Any]) -> dict[str, Any]:
194
190
  """Compute the difference between two manifests.
195
191
 
196
192
  Args:
@@ -221,11 +217,7 @@ def compute_manifest_diff(
221
217
  if new_tool["name"] in old_tools:
222
218
  # Find the corresponding old tool
223
219
  old_tool = next(
224
- (
225
- t
226
- for t in old_manifest.get("tools", [])
227
- if t["name"] == new_tool["name"]
228
- ),
220
+ (t for t in old_manifest.get("tools", []) if t["name"] == new_tool["name"]),
229
221
  None,
230
222
  )
231
223
  if old_tool and json.dumps(old_tool) != json.dumps(new_tool):
@@ -242,11 +234,7 @@ def compute_manifest_diff(
242
234
  if new_resource["name"] in old_resources:
243
235
  # Find the corresponding old resource
244
236
  old_resource = next(
245
- (
246
- r
247
- for r in old_manifest.get("resources", [])
248
- if r["name"] == new_resource["name"]
249
- ),
237
+ (r for r in old_manifest.get("resources", []) if r["name"] == new_resource["name"]),
250
238
  None,
251
239
  )
252
240
  if old_resource and json.dumps(old_resource) != json.dumps(new_resource):
@@ -263,11 +251,7 @@ def compute_manifest_diff(
263
251
  if new_prompt["name"] in old_prompts:
264
252
  # Find the corresponding old prompt
265
253
  old_prompt = next(
266
- (
267
- p
268
- for p in old_manifest.get("prompts", [])
269
- if p["name"] == new_prompt["name"]
270
- ),
254
+ (p for p in old_manifest.get("prompts", []) if p["name"] == new_prompt["name"]),
271
255
  None,
272
256
  )
273
257
  if old_prompt and json.dumps(old_prompt) != json.dumps(new_prompt):
@@ -320,7 +304,7 @@ class CodeGenerator:
320
304
  self.copy_env = copy_env
321
305
  self.components = {}
322
306
  self.manifest = {}
323
- self.common_files = {}
307
+ self.shared_files = {}
324
308
  self.import_map = {}
325
309
 
326
310
  def generate(self) -> None:
@@ -330,42 +314,39 @@ class CodeGenerator:
330
314
  self.components = parse_project(self.project_path)
331
315
  self.manifest = build_manifest(self.project_path, self.settings)
332
316
 
333
- # Find common.py files and build import map
334
- self.common_files = find_common_files(self.project_path, self.components)
335
- self.import_map = build_import_map(self.project_path, self.common_files)
317
+ # Find shared Python files and build import map
318
+ from golf.core.parser import parse_shared_files
319
+
320
+ self.shared_files = parse_shared_files(self.project_path)
321
+ self.import_map = build_import_map(self.project_path, self.shared_files)
336
322
 
337
323
  # Create output directory structure
338
324
  with console.status("Creating directory structure..."):
339
325
  self._create_directory_structure()
340
326
 
341
327
  # Generate code for all components
342
- with Progress(
343
- SpinnerColumn(),
344
- TextColumn("[bold green]Generating {task.description}"),
345
- console=console,
346
- ) as progress:
347
- tasks = [
348
- ("tools", self._generate_tools),
349
- ("resources", self._generate_resources),
350
- ("prompts", self._generate_prompts),
351
- ("server entry point", self._generate_server),
352
- ]
328
+ tasks = [
329
+ ("Generating tools", self._generate_tools),
330
+ ("Generating resources", self._generate_resources),
331
+ ("Generating prompts", self._generate_prompts),
332
+ ("Generating server entry point", self._generate_server),
333
+ ]
353
334
 
354
- for description, func in tasks:
355
- task = progress.add_task(description, total=1)
356
- func()
357
- progress.update(task, completed=1)
335
+ for description, func in tasks:
336
+ console.print(get_status_text("generating", description))
337
+ func()
358
338
 
359
339
  # Get relative path for display
360
340
  try:
361
341
  output_dir_display = self.output_dir.relative_to(Path.cwd())
362
- except ValueError:
342
+ except (ValueError, FileNotFoundError, OSError):
343
+ # ValueError: paths don't have a common base
344
+ # FileNotFoundError/OSError: current directory was deleted
363
345
  output_dir_display = self.output_dir
364
346
 
365
347
  # Show success message with output directory
366
- console.print(
367
- f"[bold green]✓[/bold green] Build completed successfully in [bold]{output_dir_display}[/bold]"
368
- )
348
+ console.print()
349
+ console.print(get_status_text("success", f"Build completed successfully in {output_dir_display}"))
369
350
 
370
351
  def _create_directory_structure(self) -> None:
371
352
  """Create the output directory structure"""
@@ -380,19 +361,20 @@ class CodeGenerator:
380
361
 
381
362
  for directory in dirs:
382
363
  directory.mkdir(parents=True, exist_ok=True)
383
- # Process common.py files directly in the components directory
384
- self._process_common_files()
364
+ # Process shared files directly in the components directory
365
+ self._process_shared_files()
385
366
 
386
- def _process_common_files(self) -> None:
387
- """Process and transform common.py files in the components directory structure."""
388
- # Reuse the already fetched common_files instead of calling the function again
389
- for dir_path_str, common_file in self.common_files.items():
390
- # Convert string path to Path object
391
- dir_path = Path(dir_path_str)
367
+ def _process_shared_files(self) -> None:
368
+ """Process and transform shared Python files in the components directory
369
+ structure."""
370
+ # Process all shared files
371
+ for module_path_str, shared_file in self.shared_files.items():
372
+ # Convert module path to Path object (e.g., "tools/weather/helpers")
373
+ module_path = Path(module_path_str)
392
374
 
393
375
  # Determine the component type
394
376
  component_type = None
395
- for part in dir_path.parts:
377
+ for part in module_path.parts:
396
378
  if part in ["tools", "resources", "prompts"]:
397
379
  component_type = part
398
380
  break
@@ -401,16 +383,14 @@ class CodeGenerator:
401
383
  continue
402
384
 
403
385
  # Calculate target directory in components structure
404
- rel_to_component = dir_path.relative_to(component_type)
405
- target_dir = (
406
- self.output_dir / "components" / component_type / rel_to_component
407
- )
386
+ rel_to_component = module_path.relative_to(component_type)
387
+ target_dir = self.output_dir / "components" / component_type / rel_to_component.parent
408
388
 
409
389
  # Create directory if it doesn't exist
410
390
  target_dir.mkdir(parents=True, exist_ok=True)
411
391
 
412
- # Create the common.py file in the target directory
413
- target_file = target_dir / "common.py"
392
+ # Create the shared file in the target directory (preserve original filename)
393
+ target_file = target_dir / shared_file.name
414
394
 
415
395
  # Use transformer to process the file
416
396
  transform_component(
@@ -418,7 +398,7 @@ class CodeGenerator:
418
398
  output_file=target_file,
419
399
  project_path=self.project_path,
420
400
  import_map=self.import_map,
421
- source_file=common_file,
401
+ source_file=shared_file,
422
402
  )
423
403
 
424
404
  def _generate_tools(self) -> None:
@@ -429,9 +409,7 @@ class CodeGenerator:
429
409
  # Get the tool directory structure
430
410
  rel_path = Path(tool.file_path).relative_to(self.project_path)
431
411
  if not rel_path.is_relative_to(Path(self.settings.tools_dir)):
432
- console.print(
433
- f"[yellow]Warning: Tool {tool.name} is not in the tools directory[/yellow]"
434
- )
412
+ console.print(f"[yellow]Warning: Tool {tool.name} is not in the tools directory[/yellow]")
435
413
  continue
436
414
 
437
415
  try:
@@ -455,9 +433,7 @@ class CodeGenerator:
455
433
  # Get the resource directory structure
456
434
  rel_path = Path(resource.file_path).relative_to(self.project_path)
457
435
  if not rel_path.is_relative_to(Path(self.settings.resources_dir)):
458
- console.print(
459
- f"[yellow]Warning: Resource {resource.name} is not in the resources directory[/yellow]"
460
- )
436
+ console.print(f"[yellow]Warning: Resource {resource.name} is not in the resources directory[/yellow]")
461
437
  continue
462
438
 
463
439
  try:
@@ -471,9 +447,7 @@ class CodeGenerator:
471
447
 
472
448
  # Create the resource file
473
449
  output_file = resource_dir / rel_path.name
474
- transform_component(
475
- resource, output_file, self.project_path, self.import_map
476
- )
450
+ transform_component(resource, output_file, self.project_path, self.import_map)
477
451
 
478
452
  def _generate_prompts(self) -> None:
479
453
  """Generate code for all prompts."""
@@ -483,9 +457,7 @@ class CodeGenerator:
483
457
  # Get the prompt directory structure
484
458
  rel_path = Path(prompt.file_path).relative_to(self.project_path)
485
459
  if not rel_path.is_relative_to(Path(self.settings.prompts_dir)):
486
- console.print(
487
- f"[yellow]Warning: Prompt {prompt.name} is not in the prompts directory[/yellow]"
488
- )
460
+ console.print(f"[yellow]Warning: Prompt {prompt.name} is not in the prompts directory[/yellow]")
489
461
  continue
490
462
 
491
463
  try:
@@ -520,16 +492,30 @@ class CodeGenerator:
520
492
  config["endpoint_path"] = "" # No HTTP endpoint
521
493
  else:
522
494
  # Default to streamable-http
523
- config["endpoint_path"] = "/mcp" # Default MCP path for FastMCP
495
+ config["endpoint_path"] = "/mcp/" # Default MCP path for FastMCP
524
496
 
525
497
  return config
526
498
 
499
+ def _is_resource_template(self, component: ParsedComponent) -> bool:
500
+ """Check if a resource component is a template (has URI parameters).
501
+
502
+ Args:
503
+ component: The parsed component to check
504
+
505
+ Returns:
506
+ True if the resource has URI parameters, False otherwise
507
+ """
508
+ return (
509
+ component.type == ComponentType.RESOURCE
510
+ and component.parameters is not None
511
+ and len(component.parameters) > 0
512
+ )
513
+
527
514
  def _generate_server(self) -> None:
528
515
  """Generate the main server entry point."""
529
516
  server_file = self.output_dir / "server.py"
530
517
 
531
518
  # Get auth components
532
- provider_config, _ = get_auth_config()
533
519
  auth_components = generate_auth_code(
534
520
  server_name=self.settings.name,
535
521
  host=self.settings.host,
@@ -542,14 +528,20 @@ class CodeGenerator:
542
528
  # Create imports section
543
529
  imports = [
544
530
  "from fastmcp import FastMCP",
531
+ "from fastmcp.tools import Tool",
532
+ "from fastmcp.resources import Resource, ResourceTemplate",
533
+ "from fastmcp.prompts import Prompt",
545
534
  "import os",
546
535
  "import sys",
547
536
  "from dotenv import load_dotenv",
548
537
  "import logging",
549
538
  "",
550
539
  "# Suppress FastMCP INFO logs",
551
- "logging.getLogger('fastmcp').setLevel(logging.WARNING)",
552
- "logging.getLogger('mcp').setLevel(logging.WARNING)",
540
+ "logging.getLogger('FastMCP').setLevel(logging.ERROR)",
541
+ "logging.getLogger('mcp').setLevel(logging.ERROR)",
542
+ "",
543
+ "# Golf utilities for MCP features (available for tool functions)",
544
+ "# from golf.utilities import elicit, sample, get_current_context",
553
545
  "",
554
546
  ]
555
547
 
@@ -615,11 +607,7 @@ class CodeGenerator:
615
607
  rel_to_tools = rel_path.relative_to(self.settings.tools_dir)
616
608
  # Handle nested directories properly
617
609
  if rel_to_tools.parent != Path("."):
618
- parent_path = (
619
- str(rel_to_tools.parent)
620
- .replace("\\", ".")
621
- .replace("/", ".")
622
- )
610
+ parent_path = str(rel_to_tools.parent).replace("\\", ".").replace("/", ".")
623
611
  import_path = f"components.tools.{parent_path}"
624
612
  else:
625
613
  import_path = "components.tools"
@@ -627,16 +615,10 @@ class CodeGenerator:
627
615
  import_path = "components.tools"
628
616
  elif component_type == ComponentType.RESOURCE:
629
617
  try:
630
- rel_to_resources = rel_path.relative_to(
631
- self.settings.resources_dir
632
- )
618
+ rel_to_resources = rel_path.relative_to(self.settings.resources_dir)
633
619
  # Handle nested directories properly
634
620
  if rel_to_resources.parent != Path("."):
635
- parent_path = (
636
- str(rel_to_resources.parent)
637
- .replace("\\", ".")
638
- .replace("/", ".")
639
- )
621
+ parent_path = str(rel_to_resources.parent).replace("\\", ".").replace("/", ".")
640
622
  import_path = f"components.resources.{parent_path}"
641
623
  else:
642
624
  import_path = "components.resources"
@@ -647,11 +629,7 @@ class CodeGenerator:
647
629
  rel_to_prompts = rel_path.relative_to(self.settings.prompts_dir)
648
630
  # Handle nested directories properly
649
631
  if rel_to_prompts.parent != Path("."):
650
- parent_path = (
651
- str(rel_to_prompts.parent)
652
- .replace("\\", ".")
653
- .replace("/", ".")
654
- )
632
+ parent_path = str(rel_to_prompts.parent).replace("\\", ".").replace("/", ".")
655
633
  import_path = f"components.prompts.{parent_path}"
656
634
  else:
657
635
  import_path = "components.prompts"
@@ -668,14 +646,10 @@ class CodeGenerator:
668
646
  # Add code to register this component
669
647
  if self.settings.opentelemetry_enabled:
670
648
  # Use telemetry instrumentation
671
- registration = (
672
- f"# Register the {component_type.value} "
673
- f"'{component.name}' with telemetry"
674
- )
649
+ registration = f"# Register the {component_type.value} '{component.name}' with telemetry"
675
650
  entry_func = (
676
651
  component.entry_function
677
- if hasattr(component, "entry_function")
678
- and component.entry_function
652
+ if hasattr(component, "entry_function") and component.entry_function
679
653
  else "export"
680
654
  )
681
655
 
@@ -686,34 +660,42 @@ class CodeGenerator:
686
660
 
687
661
  if component_type == ComponentType.TOOL:
688
662
  registration += (
689
- f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
690
- f'description="{component.docstring or ""}"'
663
+ f"\n_tool = Tool.from_function(_wrapped_func, "
664
+ f'name="{component.name}", '
665
+ f'description="{component.docstring or ""}")'
691
666
  )
692
667
  # Add annotations if present
693
668
  if hasattr(component, "annotations") and component.annotations:
694
- registration += f", annotations={component.annotations}"
695
- registration += ")"
669
+ registration += f".with_annotations({component.annotations})"
670
+ registration += "\nmcp.add_tool(_tool)"
696
671
  elif component_type == ComponentType.RESOURCE:
697
- registration += (
698
- f"\nmcp.add_resource_fn(_wrapped_func, "
699
- f'uri="{component.uri_template}", name="{component.name}", '
700
- f'description="{component.docstring or ""}")'
701
- )
672
+ if self._is_resource_template(component):
673
+ registration += (
674
+ f"\n_template = ResourceTemplate.from_function(_wrapped_func, "
675
+ f'uri_template="{component.uri_template}", name="{component.name}", '
676
+ f'description="{component.docstring or ""}")\n'
677
+ f"mcp.add_template(_template)"
678
+ )
679
+ else:
680
+ registration += (
681
+ f"\n_resource = Resource.from_function(_wrapped_func, "
682
+ f'uri="{component.uri_template}", name="{component.name}", '
683
+ f'description="{component.docstring or ""}")\n'
684
+ f"mcp.add_resource(_resource)"
685
+ )
702
686
  else: # PROMPT
703
687
  registration += (
704
- f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
705
- f'description="{component.docstring or ""}")'
688
+ f"\n_prompt = Prompt.from_function(_wrapped_func, "
689
+ f'name="{component.name}", '
690
+ f'description="{component.docstring or ""}")\n'
691
+ f"mcp.add_prompt(_prompt)"
706
692
  )
707
693
  elif self.settings.metrics_enabled:
708
694
  # Use metrics instrumentation
709
- registration = (
710
- f"# Register the {component_type.value} "
711
- f"'{component.name}' with metrics"
712
- )
695
+ registration = f"# Register the {component_type.value} '{component.name}' with metrics"
713
696
  entry_func = (
714
697
  component.entry_function
715
- if hasattr(component, "entry_function")
716
- and component.entry_function
698
+ if hasattr(component, "entry_function") and component.entry_function
717
699
  else "export"
718
700
  )
719
701
 
@@ -724,37 +706,49 @@ class CodeGenerator:
724
706
 
725
707
  if component_type == ComponentType.TOOL:
726
708
  registration += (
727
- f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
728
- f'description="{component.docstring or ""}"'
709
+ f"\n_tool = Tool.from_function(_wrapped_func, "
710
+ f'name="{component.name}", '
711
+ f'description="{component.docstring or ""}")'
729
712
  )
730
713
  # Add annotations if present
731
714
  if hasattr(component, "annotations") and component.annotations:
732
- registration += f", annotations={component.annotations}"
733
- registration += ")"
715
+ registration += f".with_annotations({component.annotations})"
716
+ registration += "\nmcp.add_tool(_tool)"
734
717
  elif component_type == ComponentType.RESOURCE:
735
- registration += (
736
- f"\nmcp.add_resource_fn(_wrapped_func, "
737
- f'uri="{component.uri_template}", name="{component.name}", '
738
- f'description="{component.docstring or ""}")'
739
- )
718
+ if self._is_resource_template(component):
719
+ registration += (
720
+ f"\n_template = ResourceTemplate.from_function(_wrapped_func, "
721
+ f'uri_template="{component.uri_template}", name="{component.name}", '
722
+ f'description="{component.docstring or ""}")\n'
723
+ f"mcp.add_template(_template)"
724
+ )
725
+ else:
726
+ registration += (
727
+ f"\n_resource = Resource.from_function(_wrapped_func, "
728
+ f'uri="{component.uri_template}", name="{component.name}", '
729
+ f'description="{component.docstring or ""}")\n'
730
+ f"mcp.add_resource(_resource)"
731
+ )
740
732
  else: # PROMPT
741
733
  registration += (
742
- f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
743
- f'description="{component.docstring or ""}")'
734
+ f"\n_prompt = Prompt.from_function(_wrapped_func, "
735
+ f'name="{component.name}", '
736
+ f'description="{component.docstring or ""}")\n'
737
+ f"mcp.add_prompt(_prompt)"
744
738
  )
745
739
  else:
746
740
  # Standard registration without telemetry
747
741
  if component_type == ComponentType.TOOL:
748
742
  registration = f"# Register the tool '{component.name}' from {full_module_path}"
749
743
 
750
- # Use the entry_function if available, otherwise try the export variable
751
- if (
752
- hasattr(component, "entry_function")
753
- and component.entry_function
754
- ):
755
- registration += f"\nmcp.add_tool({full_module_path}.{component.entry_function}"
744
+ # Use the entry_function if available, otherwise try the
745
+ # export variable
746
+ if hasattr(component, "entry_function") and component.entry_function:
747
+ registration += (
748
+ f"\n_tool = Tool.from_function({full_module_path}.{component.entry_function}"
749
+ )
756
750
  else:
757
- registration += f"\nmcp.add_tool({full_module_path}.export"
751
+ registration += f"\n_tool = Tool.from_function({full_module_path}.export"
758
752
 
759
753
  # Add the name parameter
760
754
  registration += f', name="{component.name}"'
@@ -765,48 +759,85 @@ class CodeGenerator:
765
759
  escaped_docstring = component.docstring.replace('"', '\\"')
766
760
  registration += f', description="{escaped_docstring}"'
767
761
 
762
+ registration += ")"
763
+
768
764
  # Add annotations if present
769
765
  if hasattr(component, "annotations") and component.annotations:
770
- registration += f", annotations={component.annotations}"
766
+ registration += f"\n_tool = _tool.with_annotations({component.annotations})"
771
767
 
772
- registration += ")"
768
+ registration += "\nmcp.add_tool(_tool)"
773
769
 
774
770
  elif component_type == ComponentType.RESOURCE:
775
- registration = f"# Register the resource '{component.name}' from {full_module_path}"
776
-
777
- # Use the entry_function if available, otherwise try the export variable
778
- if (
779
- hasattr(component, "entry_function")
780
- and component.entry_function
781
- ):
782
- registration += f'\nmcp.add_resource_fn({full_module_path}.{component.entry_function}, uri="{component.uri_template}"'
783
- else:
784
- registration += f'\nmcp.add_resource_fn({full_module_path}.export, uri="{component.uri_template}"'
785
-
786
- # Add the name parameter
787
- registration += f', name="{component.name}"'
788
-
789
- # Add description from docstring
790
- if component.docstring:
791
- # Escape any quotes in the docstring
792
- escaped_docstring = component.docstring.replace('"', '\\"')
793
- registration += f', description="{escaped_docstring}"'
771
+ if self._is_resource_template(component):
772
+ registration = (
773
+ f"# Register the resource template '{component.name}' from {full_module_path}"
774
+ )
794
775
 
795
- registration += ")"
776
+ # Use the entry_function if available, otherwise try the
777
+ # export variable
778
+ if hasattr(component, "entry_function") and component.entry_function:
779
+ registration += (
780
+ f"\n_template = ResourceTemplate.from_function("
781
+ f"{full_module_path}.{component.entry_function}, "
782
+ f'uri_template="{component.uri_template}"'
783
+ )
784
+ else:
785
+ registration += (
786
+ f"\n_template = ResourceTemplate.from_function("
787
+ f"{full_module_path}.export, "
788
+ f'uri_template="{component.uri_template}"'
789
+ )
790
+
791
+ # Add the name parameter
792
+ registration += f', name="{component.name}"'
793
+
794
+ # Add description from docstring
795
+ if component.docstring:
796
+ # Escape any quotes in the docstring
797
+ escaped_docstring = component.docstring.replace('"', '\\"')
798
+ registration += f', description="{escaped_docstring}"'
799
+
800
+ registration += ")\nmcp.add_template(_template)"
801
+ else:
802
+ registration = f"# Register the resource '{component.name}' from {full_module_path}"
803
+
804
+ # Use the entry_function if available, otherwise try the
805
+ # export variable
806
+ if hasattr(component, "entry_function") and component.entry_function:
807
+ registration += (
808
+ f"\n_resource = Resource.from_function("
809
+ f"{full_module_path}.{component.entry_function}, "
810
+ f'uri="{component.uri_template}"'
811
+ )
812
+ else:
813
+ registration += (
814
+ f"\n_resource = Resource.from_function("
815
+ f"{full_module_path}.export, "
816
+ f'uri="{component.uri_template}"'
817
+ )
818
+
819
+ # Add the name parameter
820
+ registration += f', name="{component.name}"'
821
+
822
+ # Add description from docstring
823
+ if component.docstring:
824
+ # Escape any quotes in the docstring
825
+ escaped_docstring = component.docstring.replace('"', '\\"')
826
+ registration += f', description="{escaped_docstring}"'
827
+
828
+ registration += ")\nmcp.add_resource(_resource)"
796
829
 
797
830
  else: # PROMPT
798
831
  registration = f"# Register the prompt '{component.name}' from {full_module_path}"
799
832
 
800
- # Use the entry_function if available, otherwise try the export variable
801
- if (
802
- hasattr(component, "entry_function")
803
- and component.entry_function
804
- ):
805
- registration += f"\nmcp.add_prompt({full_module_path}.{component.entry_function}"
806
- else:
833
+ # Use the entry_function if available, otherwise try the
834
+ # export variable
835
+ if hasattr(component, "entry_function") and component.entry_function:
807
836
  registration += (
808
- f"\nmcp.add_prompt({full_module_path}.export"
837
+ f"\n_prompt = Prompt.from_function({full_module_path}.{component.entry_function}"
809
838
  )
839
+ else:
840
+ registration += f"\n_prompt = Prompt.from_function({full_module_path}.export"
810
841
 
811
842
  # Add the name parameter
812
843
  registration += f', name="{component.name}"'
@@ -817,7 +848,7 @@ class CodeGenerator:
817
848
  escaped_docstring = component.docstring.replace('"', '\\"')
818
849
  registration += f', description="{escaped_docstring}"'
819
850
 
820
- registration += ")"
851
+ registration += ")\nmcp.add_prompt(_prompt)"
821
852
 
822
853
  component_registrations.append(registration)
823
854
 
@@ -870,8 +901,9 @@ class CodeGenerator:
870
901
  early_telemetry_init.extend(
871
902
  [
872
903
  "# Initialize telemetry early to ensure instrumentation works",
873
- "from golf.telemetry.instrumentation import init_telemetry",
904
+ "from golf.telemetry.instrumentation import init_telemetry, set_detailed_tracing",
874
905
  f'init_telemetry("{self.settings.name}")',
906
+ f"set_detailed_tracing({self.settings.detailed_tracing})",
875
907
  "",
876
908
  ]
877
909
  )
@@ -881,9 +913,7 @@ class CodeGenerator:
881
913
  if self.settings.metrics_enabled:
882
914
  from golf.core.builder_metrics import generate_metrics_initialization
883
915
 
884
- early_metrics_init.extend(
885
- generate_metrics_initialization(self.settings.name)
886
- )
916
+ early_metrics_init.extend(generate_metrics_initialization(self.settings.name))
887
917
 
888
918
  # Main entry point with transport-specific app initialization
889
919
  main_code = [
@@ -892,22 +922,12 @@ class CodeGenerator:
892
922
  " from rich.panel import Panel",
893
923
  " console = Console()",
894
924
  " # Get configuration from environment variables or use defaults",
895
- ' host = os.environ.get("HOST", "127.0.0.1")',
925
+ ' host = os.environ.get("HOST", "localhost")',
896
926
  ' port = int(os.environ.get("PORT", 3000))',
897
927
  f' transport_to_run = "{self.settings.transport}"',
898
928
  "",
899
929
  ]
900
930
 
901
- # Add startup message
902
- if self.settings.transport != "stdio":
903
- main_code.append(
904
- f' console.print(Panel.fit(f"[bold green]{{mcp.name}}[/bold green]\\n[dim]Running on http://{{host}}:{{port}}{endpoint_path} with transport \\"{{transport_to_run}}\\" (environment: {self.build_env})[/dim]", border_style="green"))'
905
- )
906
- else:
907
- main_code.append(
908
- f' console.print(Panel.fit(f"[bold green]{{mcp.name}}[/bold green]\\n[dim]Running with transport \\"{{transport_to_run}}\\" (environment: {self.build_env})[/dim]", border_style="green"))'
909
- )
910
-
911
931
  main_code.append("")
912
932
 
913
933
  # Transport-specific run methods
@@ -918,26 +938,18 @@ class CodeGenerator:
918
938
 
919
939
  api_key_config = get_api_key_config()
920
940
  if auth_components.get("has_auth") and api_key_config:
921
- middleware_setup.append(
922
- " from starlette.middleware import Middleware"
923
- )
941
+ middleware_setup.append(" from starlette.middleware import Middleware")
924
942
  middleware_list.append("Middleware(ApiKeyMiddleware)")
925
943
 
926
944
  # Add metrics middleware if enabled
927
945
  if self.settings.metrics_enabled:
928
- middleware_setup.append(
929
- " from starlette.middleware import Middleware"
930
- )
946
+ middleware_setup.append(" from starlette.middleware import Middleware")
931
947
  middleware_list.append("Middleware(MetricsMiddleware)")
932
948
 
933
949
  # Add OpenTelemetry middleware if enabled
934
950
  if self.settings.opentelemetry_enabled:
935
- middleware_setup.append(
936
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
937
- )
938
- middleware_setup.append(
939
- " from starlette.middleware import Middleware"
940
- )
951
+ middleware_setup.append(" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware")
952
+ middleware_setup.append(" from starlette.middleware import Middleware")
941
953
  middleware_list.append("Middleware(OpenTelemetryMiddleware)")
942
954
 
943
955
  if middleware_setup:
@@ -947,14 +959,18 @@ class CodeGenerator:
947
959
  main_code.extend(
948
960
  [
949
961
  " # Run SSE server with middleware using FastMCP's run method",
950
- ' mcp.run(transport="sse", host=host, port=port, log_level="info", middleware=middleware)',
962
+ f' mcp.run(transport="sse", host=host, port=port, '
963
+ f'path="{endpoint_path}", log_level="info", '
964
+ f"middleware=middleware, show_banner=False)",
951
965
  ]
952
966
  )
953
967
  else:
954
968
  main_code.extend(
955
969
  [
956
970
  " # Run SSE server using FastMCP's run method",
957
- ' mcp.run(transport="sse", host=host, port=port, log_level="info")',
971
+ f' mcp.run(transport="sse", host=host, port=port, '
972
+ f'path="{endpoint_path}", log_level="info", '
973
+ f"show_banner=False)",
958
974
  ]
959
975
  )
960
976
 
@@ -965,26 +981,18 @@ class CodeGenerator:
965
981
 
966
982
  api_key_config = get_api_key_config()
967
983
  if auth_components.get("has_auth") and api_key_config:
968
- middleware_setup.append(
969
- " from starlette.middleware import Middleware"
970
- )
984
+ middleware_setup.append(" from starlette.middleware import Middleware")
971
985
  middleware_list.append("Middleware(ApiKeyMiddleware)")
972
986
 
973
987
  # Add metrics middleware if enabled
974
988
  if self.settings.metrics_enabled:
975
- middleware_setup.append(
976
- " from starlette.middleware import Middleware"
977
- )
989
+ middleware_setup.append(" from starlette.middleware import Middleware")
978
990
  middleware_list.append("Middleware(MetricsMiddleware)")
979
991
 
980
992
  # Add OpenTelemetry middleware if enabled
981
993
  if self.settings.opentelemetry_enabled:
982
- middleware_setup.append(
983
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware"
984
- )
985
- middleware_setup.append(
986
- " from starlette.middleware import Middleware"
987
- )
994
+ middleware_setup.append(" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware")
995
+ middleware_setup.append(" from starlette.middleware import Middleware")
988
996
  middleware_list.append("Middleware(OpenTelemetryMiddleware)")
989
997
 
990
998
  if middleware_setup:
@@ -994,21 +1002,23 @@ class CodeGenerator:
994
1002
  main_code.extend(
995
1003
  [
996
1004
  " # Run HTTP server with middleware using FastMCP's run method",
997
- ' mcp.run(transport="streamable-http", host=host, port=port, log_level="info", middleware=middleware)',
1005
+ f' mcp.run(transport="streamable-http", host=host, '
1006
+ f'port=port, path="{endpoint_path}", log_level="info", '
1007
+ f"middleware=middleware, show_banner=False)",
998
1008
  ]
999
1009
  )
1000
1010
  else:
1001
1011
  main_code.extend(
1002
1012
  [
1003
1013
  " # Run HTTP server using FastMCP's run method",
1004
- ' mcp.run(transport="streamable-http", host=host, port=port, log_level="info")',
1014
+ f' mcp.run(transport="streamable-http", host=host, '
1015
+ f'port=port, path="{endpoint_path}", log_level="info", '
1016
+ f"show_banner=False)",
1005
1017
  ]
1006
1018
  )
1007
1019
  else:
1008
1020
  # For stdio transport, use mcp.run()
1009
- main_code.extend(
1010
- [" # Run with stdio transport", ' mcp.run(transport="stdio")']
1011
- )
1021
+ main_code.extend([" # Run with stdio transport", ' mcp.run(transport="stdio", show_banner=False)'])
1012
1022
 
1013
1023
  # Add metrics route if enabled
1014
1024
  metrics_route_code = []
@@ -1022,18 +1032,17 @@ class CodeGenerator:
1022
1032
  if self.settings.health_check_enabled:
1023
1033
  health_check_code = [
1024
1034
  "# Add health check route",
1025
- "@mcp.custom_route('"
1026
- + self.settings.health_check_path
1027
- + '\', methods=["GET"])',
1035
+ "@mcp.custom_route('" + self.settings.health_check_path + '\', methods=["GET"])',
1028
1036
  "async def health_check(request: Request) -> PlainTextResponse:",
1029
1037
  ' """Health check endpoint for Kubernetes and load balancers."""',
1030
- f' return PlainTextResponse("{self.settings.health_check_response}")',
1038
+ (f' return PlainTextResponse("{self.settings.health_check_response}")'),
1031
1039
  "",
1032
1040
  ]
1033
1041
 
1034
1042
  # Combine all sections
1035
1043
  # Order: imports, env_section, auth_setup, server_code (mcp init),
1036
- # early_telemetry_init, early_metrics_init, component_registrations, metrics_route_code, health_check_code, main_code (run block)
1044
+ # early_telemetry_init, early_metrics_init, component_registrations,
1045
+ # metrics_route_code, health_check_code, main_code (run block)
1037
1046
  code = "\n".join(
1038
1047
  imports
1039
1048
  + env_section
@@ -1089,66 +1098,81 @@ def build_project(
1089
1098
  if has_api_key and has_server_id:
1090
1099
  console.print("[dim]Loaded Golf credentials for build operations[/dim]")
1091
1100
 
1092
- # Execute pre_build.py if it exists
1093
- pre_build_path = project_path / "pre_build.py"
1094
- if pre_build_path.exists():
1101
+ # Execute auth.py if it exists (for authentication configuration)
1102
+ # Also support legacy pre_build.py for backward compatibility
1103
+ auth_path = project_path / "auth.py"
1104
+ legacy_path = project_path / "pre_build.py"
1105
+
1106
+ config_path = None
1107
+ if auth_path.exists():
1108
+ config_path = auth_path
1109
+ elif legacy_path.exists():
1110
+ config_path = legacy_path
1111
+ console.print("[yellow]Warning: pre_build.py is deprecated. Rename to auth.py[/yellow]")
1112
+
1113
+ if config_path:
1114
+ # Save the current directory and path - handle case where cwd might be invalid
1095
1115
  try:
1096
- # Save the current directory and path
1097
1116
  original_dir = os.getcwd()
1098
- original_path = sys.path.copy()
1117
+ except (FileNotFoundError, OSError):
1118
+ # Current directory might have been deleted by previous operations,
1119
+ # use project_path as fallback
1120
+ original_dir = str(project_path)
1121
+ os.chdir(original_dir)
1122
+ original_path = sys.path.copy()
1099
1123
 
1124
+ try:
1100
1125
  # Change to the project directory and add it to Python path
1101
1126
  os.chdir(project_path)
1102
1127
  sys.path.insert(0, str(project_path))
1103
1128
 
1104
- # Execute the pre_build script
1105
- with open(pre_build_path) as f:
1129
+ # Execute the auth configuration script
1130
+ with open(config_path) as f:
1106
1131
  script_content = f.read()
1107
1132
 
1108
1133
  # Print the first few lines for debugging
1109
1134
  "\n".join(script_content.split("\n")[:5]) + "\n..."
1110
1135
 
1111
1136
  # Use exec to run the script as a module
1112
- code = compile(script_content, str(pre_build_path), "exec")
1137
+ code = compile(script_content, str(config_path), "exec")
1113
1138
  exec(code, {})
1114
1139
 
1115
- # Check if auth was configured by the script
1116
- provider, scopes = get_auth_config()
1117
-
1118
- # Restore original directory and path
1119
- os.chdir(original_dir)
1120
- sys.path = original_path
1121
-
1122
1140
  except Exception as e:
1123
- console.print(f"[red]Error executing pre_build.py: {str(e)}[/red]")
1141
+ console.print(f"[red]Error executing {config_path.name}: {str(e)}[/red]")
1124
1142
  import traceback
1125
1143
 
1126
1144
  console.print(f"[red]{traceback.format_exc()}[/red]")
1127
1145
 
1128
- # Track detailed error for pre_build.py execution failures
1146
+ # Track detailed error for auth.py execution failures
1129
1147
  try:
1130
1148
  from golf.core.telemetry import track_detailed_error
1131
1149
 
1132
1150
  track_detailed_error(
1133
- "build_pre_build_failed",
1151
+ "build_auth_failed",
1134
1152
  e,
1135
- context="Executing pre_build.py configuration script",
1136
- operation="pre_build_execution",
1153
+ context=f"Executing {config_path.name} configuration script",
1154
+ operation="auth_execution",
1137
1155
  additional_props={
1138
- "file_path": str(pre_build_path.relative_to(project_path)),
1156
+ "file_path": str(config_path.relative_to(project_path)),
1139
1157
  "build_env": build_env,
1140
1158
  },
1141
1159
  )
1142
1160
  except Exception:
1143
1161
  # Don't let telemetry errors break the build
1144
1162
  pass
1163
+ finally:
1164
+ # Always restore original directory and path, even if an exception occurred
1165
+ try:
1166
+ os.chdir(original_dir)
1167
+ sys.path = original_path
1168
+ except Exception:
1169
+ # If we can't restore the directory, at least try to reset the path
1170
+ sys.path = original_path
1145
1171
 
1146
1172
  # Clear the output directory if it exists
1147
1173
  if output_dir.exists():
1148
1174
  shutil.rmtree(output_dir)
1149
- output_dir.mkdir(
1150
- parents=True, exist_ok=True
1151
- ) # Ensure output_dir exists after clearing
1175
+ output_dir.mkdir(parents=True, exist_ok=True) # Ensure output_dir exists after clearing
1152
1176
 
1153
1177
  # --- BEGIN Enhanced .env handling ---
1154
1178
  env_vars_to_write = {}
@@ -1164,38 +1188,36 @@ def build_project(
1164
1188
  env_vars_to_write.update(dotenv_values(project_env_file))
1165
1189
  except ImportError:
1166
1190
  console.print(
1167
- "[yellow]Warning: python-dotenv is not installed. Cannot read existing .env file for rich merging. Copying directly.[/yellow]"
1191
+ "[yellow]Warning: python-dotenv is not installed. "
1192
+ "Cannot read existing .env file for rich merging. "
1193
+ "Copying directly.[/yellow]"
1168
1194
  )
1169
1195
  try:
1170
1196
  shutil.copy(project_env_file, env_file_path)
1171
- # If direct copy happens, re-read for step 2 & 3 to respect its content
1197
+ # If direct copy happens, re-read for step 2 & 3 to respect
1198
+ # its content
1172
1199
  if env_file_path.exists():
1173
1200
  from dotenv import dotenv_values
1174
1201
 
1175
- env_vars_to_write.update(
1176
- dotenv_values(env_file_path)
1177
- ) # Read what was copied
1202
+ env_vars_to_write.update(dotenv_values(env_file_path)) # Read what was copied
1178
1203
  except Exception as e:
1179
- console.print(
1180
- f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]"
1181
- )
1204
+ console.print(f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]")
1182
1205
  except Exception as e:
1183
- console.print(
1184
- f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]"
1185
- )
1206
+ console.print(f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]")
1186
1207
 
1187
- # 2. Apply Golf's OTel default exporter setting if OTEL_TRACES_EXPORTER is not already set
1188
- if settings.opentelemetry_enabled and settings.opentelemetry_default_exporter:
1189
- if "OTEL_TRACES_EXPORTER" not in env_vars_to_write:
1190
- env_vars_to_write["OTEL_TRACES_EXPORTER"] = (
1191
- settings.opentelemetry_default_exporter
1192
- )
1208
+ # 2. Apply Golf's OTel default exporter setting if OTEL_TRACES_EXPORTER
1209
+ # is not already set
1210
+ if (
1211
+ settings.opentelemetry_enabled
1212
+ and settings.opentelemetry_default_exporter
1213
+ and "OTEL_TRACES_EXPORTER" not in env_vars_to_write
1214
+ ):
1215
+ env_vars_to_write["OTEL_TRACES_EXPORTER"] = settings.opentelemetry_default_exporter
1193
1216
 
1194
1217
  # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
1195
1218
  # (Ensures service name defaults to project name if not specified in user's .env)
1196
- if settings.opentelemetry_enabled and settings.name:
1197
- if "OTEL_SERVICE_NAME" not in env_vars_to_write:
1198
- env_vars_to_write["OTEL_SERVICE_NAME"] = settings.name
1219
+ if settings.opentelemetry_enabled and settings.name and "OTEL_SERVICE_NAME" not in env_vars_to_write:
1220
+ env_vars_to_write["OTEL_SERVICE_NAME"] = settings.name
1199
1221
 
1200
1222
  # 4. (Re-)Write the .env file in the output directory if there's anything to write
1201
1223
  if env_vars_to_write:
@@ -1206,73 +1228,51 @@ def build_project(
1206
1228
  # and handle existing quotes within the value.
1207
1229
  if isinstance(value, str):
1208
1230
  # Replace backslashes first, then double quotes
1209
- processed_value = value.replace(
1210
- "\\", "\\\\"
1211
- ) # Escape backslashes
1212
- processed_value = processed_value.replace(
1213
- '"', '\\"'
1214
- ) # Escape double quotes
1215
- if (
1216
- " " in value
1217
- or "#" in value
1218
- or "\n" in value
1219
- or '"' in value
1220
- or "'" in value
1221
- ):
1231
+ processed_value = value.replace("\\", "\\\\") # Escape backslashes
1232
+ processed_value = processed_value.replace('"', '\\"') # Escape double quotes
1233
+ if " " in value or "#" in value or "\n" in value or '"' in value or "'" in value:
1222
1234
  f.write(f'{key}="{processed_value}"\n')
1223
1235
  else:
1224
1236
  f.write(f"{key}={processed_value}\n")
1225
1237
  else: # For non-string values, write directly
1226
1238
  f.write(f"{key}={value}\n")
1227
1239
  except Exception as e:
1228
- console.print(
1229
- f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]"
1230
- )
1240
+ console.print(f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]")
1231
1241
  # --- END Enhanced .env handling ---
1232
1242
 
1233
1243
  # Show what we're building, with environment info
1234
- console.print(
1235
- f"[bold]Building [green]{settings.name}[/green] ({build_env} environment)[/bold]"
1236
- )
1244
+ create_build_header(settings.name, build_env, console)
1237
1245
 
1238
1246
  # Generate the code
1239
- generator = CodeGenerator(
1240
- project_path, settings, output_dir, build_env=build_env, copy_env=copy_env
1241
- )
1247
+ generator = CodeGenerator(project_path, settings, output_dir, build_env=build_env, copy_env=copy_env)
1242
1248
  generator.generate()
1243
1249
 
1244
1250
  # Platform registration (only for prod builds)
1245
1251
  if build_env == "prod":
1246
- console.print(
1247
- "[dim]Registering with Golf platform and updating resources...[/dim]"
1248
- )
1249
- import asyncio
1252
+ console.print()
1253
+ status_msg = f"[{GOLF_BLUE}]{STATUS_ICONS['platform']} Registering with Golf platform and updating resources...[/{GOLF_BLUE}]"
1254
+ with console.status(status_msg):
1255
+ import asyncio
1250
1256
 
1251
- try:
1252
- from golf.core.platform import register_project_with_platform
1257
+ try:
1258
+ from golf.core.platform import register_project_with_platform
1253
1259
 
1254
- success = asyncio.run(
1255
- register_project_with_platform(
1256
- project_path=project_path,
1257
- settings=settings,
1258
- components=generator.components,
1260
+ success = asyncio.run(
1261
+ register_project_with_platform(
1262
+ project_path=project_path,
1263
+ settings=settings,
1264
+ components=generator.components,
1265
+ )
1259
1266
  )
1260
- )
1261
1267
 
1262
- if success:
1263
- console.print("[green]✓ Platform registration completed[/green]")
1264
- # If success is False, the platform module already printed appropriate warnings
1265
- except ImportError:
1266
- console.print(
1267
- "[yellow]Warning: Platform registration module not available[/yellow]"
1268
- )
1269
- except Exception as e:
1270
- console.print(
1271
- f"[yellow]Warning: Platform registration failed: {e}[/yellow]"
1272
- )
1273
- console.print(
1274
- "[yellow]Tip: Ensure GOLF_API_KEY and GOLF_SERVER_ID are available in your .env file[/yellow]"
1275
- )
1268
+ if success:
1269
+ console.print(get_status_text("success", "Platform registration completed"))
1270
+ # If success is False, the platform module already printed appropriate warnings
1271
+ except ImportError:
1272
+ console.print(get_status_text("warning", "Platform registration module not available"))
1273
+ except Exception as e:
1274
+ console.print(get_status_text("warning", f"Platform registration failed: {e}"))
1275
+ console.print("[dim]Tip: Ensure GOLF_API_KEY and GOLF_SERVER_ID are available in your .env file[/dim]")
1276
1276
 
1277
1277
  # Create a simple README
1278
1278
  readme_content = f"""# {settings.name}
@@ -1292,50 +1292,6 @@ This is a standalone FastMCP server generated by GolfMCP.
1292
1292
  with open(output_dir / "README.md", "w") as f:
1293
1293
  f.write(readme_content)
1294
1294
 
1295
- # Copy pyproject.toml with required dependencies
1296
- base_dependencies = [
1297
- "fastmcp>=2.0.0,<2.6.0",
1298
- "uvicorn>=0.20.0",
1299
- "pydantic>=2.0.0",
1300
- "python-dotenv>=1.0.0",
1301
- ]
1302
-
1303
- # Add OpenTelemetry dependencies if enabled
1304
- if settings.opentelemetry_enabled:
1305
- base_dependencies.extend(get_otel_dependencies())
1306
-
1307
- # Add authentication dependencies if enabled, before generating pyproject_content
1308
- provider_config, required_scopes = (
1309
- get_auth_config()
1310
- ) # Ensure this is called to check for auth
1311
- if provider_config:
1312
- base_dependencies.extend(
1313
- [
1314
- "pyjwt>=2.0.0",
1315
- "httpx>=0.20.0",
1316
- ]
1317
- )
1318
-
1319
- # Create the dependencies string
1320
- dependencies_str = ",\n ".join([f'"{dep}"' for dep in base_dependencies])
1321
-
1322
- pyproject_content = f"""[build-system]
1323
- requires = ["setuptools>=61.0"]
1324
- build-backend = "setuptools.build_meta"
1325
-
1326
- [project]
1327
- name = "generated-fastmcp-app"
1328
- version = "0.1.0"
1329
- description = "Generated FastMCP Application"
1330
- requires-python = ">=3.10"
1331
- dependencies = [
1332
- {dependencies_str}
1333
- ]
1334
- """
1335
-
1336
- with open(output_dir / "pyproject.toml", "w") as f:
1337
- f.write(pyproject_content)
1338
-
1339
1295
  # Always copy the auth module so it's available
1340
1296
  auth_dir = output_dir / "golf" / "auth"
1341
1297
  auth_dir.mkdir(parents=True, exist_ok=True)
@@ -1345,24 +1301,24 @@ dependencies = [
1345
1301
  f.write(
1346
1302
  """\"\"\"Auth module for GolfMCP.\"\"\"
1347
1303
 
1348
- from golf.auth.provider import ProviderConfig
1349
- from golf.auth.oauth import GolfOAuthProvider, create_callback_handler
1350
- from golf.auth.helpers import get_access_token, get_provider_token, extract_token_from_header, get_api_key, set_api_key
1304
+ # Legacy ProviderConfig removed in Golf 0.2.x - use modern auth configurations
1305
+ # Legacy OAuth imports removed in Golf 0.2.x - use FastMCP 2.11+ auth providers
1306
+ from golf.auth.helpers import get_provider_token, extract_token_from_header, get_api_key, set_api_key
1351
1307
  from golf.auth.api_key import configure_api_key, get_api_key_config
1308
+ from golf.auth.factory import create_auth_provider
1309
+ from golf.auth.providers import RemoteAuthConfig, JWTAuthConfig, StaticTokenConfig, OAuthServerConfig
1352
1310
  """
1353
1311
  )
1354
1312
 
1355
- # Copy provider, oauth, and helper modules
1356
- for module in ["provider.py", "oauth.py", "helpers.py", "api_key.py"]:
1313
+ # Copy auth modules required for Golf 0.2.x
1314
+ for module in ["helpers.py", "api_key.py", "factory.py", "providers.py"]:
1357
1315
  src_file = Path(__file__).parent.parent.parent / "golf" / "auth" / module
1358
1316
  dst_file = auth_dir / module
1359
1317
 
1360
1318
  if src_file.exists():
1361
1319
  shutil.copy(src_file, dst_file)
1362
1320
  else:
1363
- console.print(
1364
- f"[yellow]Warning: Could not find {src_file} to copy[/yellow]"
1365
- )
1321
+ console.print(f"[yellow]Warning: Could not find {src_file} to copy[/yellow]")
1366
1322
 
1367
1323
  # Copy telemetry module if OpenTelemetry is enabled
1368
1324
  if settings.opentelemetry_enabled:
@@ -1370,31 +1326,21 @@ from golf.auth.api_key import configure_api_key, get_api_key_config
1370
1326
  telemetry_dir.mkdir(parents=True, exist_ok=True)
1371
1327
 
1372
1328
  # Copy telemetry __init__.py
1373
- src_init = (
1374
- Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1375
- )
1329
+ src_init = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1376
1330
  dst_init = telemetry_dir / "__init__.py"
1377
1331
  if src_init.exists():
1378
1332
  shutil.copy(src_init, dst_init)
1379
1333
 
1380
1334
  # Copy instrumentation module
1381
- src_instrumentation = (
1382
- Path(__file__).parent.parent.parent
1383
- / "golf"
1384
- / "telemetry"
1385
- / "instrumentation.py"
1386
- )
1335
+ src_instrumentation = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "instrumentation.py"
1387
1336
  dst_instrumentation = telemetry_dir / "instrumentation.py"
1388
1337
  if src_instrumentation.exists():
1389
1338
  shutil.copy(src_instrumentation, dst_instrumentation)
1390
1339
  else:
1391
- console.print(
1392
- "[yellow]Warning: Could not find telemetry instrumentation module[/yellow]"
1393
- )
1340
+ console.print("[yellow]Warning: Could not find telemetry instrumentation module[/yellow]")
1394
1341
 
1395
1342
  # Check if auth routes need to be added
1396
- provider_config, _ = get_auth_config()
1397
- if provider_config:
1343
+ if is_auth_configured() or get_api_key_config():
1398
1344
  auth_routes_code = generate_auth_routes()
1399
1345
 
1400
1346
  server_file = output_dir / "server.py"
@@ -1407,17 +1353,12 @@ from golf.auth.api_key import configure_api_key, get_api_key_config
1407
1353
  app_pos = server_code_content.find(app_marker)
1408
1354
  if app_pos != -1:
1409
1355
  modified_code = (
1410
- server_code_content[:app_pos]
1411
- + auth_routes_code
1412
- + "\n\n"
1413
- + server_code_content[app_pos:]
1356
+ server_code_content[:app_pos] + auth_routes_code + "\n\n" + server_code_content[app_pos:]
1414
1357
  )
1415
1358
 
1416
1359
  # Format with black before writing
1417
1360
  try:
1418
- final_code_to_write = black.format_str(
1419
- modified_code, mode=black.Mode()
1420
- )
1361
+ final_code_to_write = black.format_str(modified_code, mode=black.Mode())
1421
1362
  except Exception as e:
1422
1363
  console.print(
1423
1364
  f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]"
@@ -1432,38 +1373,29 @@ from golf.auth.api_key import configure_api_key, get_api_key_config
1432
1373
  )
1433
1374
 
1434
1375
 
1435
- # Renamed function - was find_shared_modules
1436
- def find_common_files(
1437
- project_path: Path, components: dict[ComponentType, list[ParsedComponent]]
1438
- ) -> dict[str, Path]:
1439
- """Find all common.py files used by components."""
1440
- # We'll use the parser's functionality to find common files directly
1441
- from golf.core.parser import parse_common_files
1442
-
1443
- common_files = parse_common_files(project_path)
1444
-
1445
- # Return the found files without debug messages
1446
- return common_files
1376
+ # Legacy function removed - replaced by parse_shared_files in parser module
1447
1377
 
1448
1378
 
1449
- # Updated parameter name from shared_modules to common_files
1450
- def build_import_map(
1451
- project_path: Path, common_files: dict[str, Path]
1452
- ) -> dict[str, str]:
1379
+ # Updated to handle any shared file, not just common.py files
1380
+ def build_import_map(project_path: Path, shared_files: dict[str, Path]) -> dict[str, str]:
1453
1381
  """Build a mapping of import paths to their new locations in the build output.
1454
1382
 
1455
1383
  This maps from original relative import paths to absolute import paths
1456
1384
  in the components directory structure.
1385
+
1386
+ Args:
1387
+ project_path: Path to the project root
1388
+ shared_files: Dictionary mapping module paths to shared file paths
1457
1389
  """
1458
1390
  import_map = {}
1459
1391
 
1460
- for dir_path_str, _file_path in common_files.items():
1461
- # Convert string path to Path object
1462
- dir_path = Path(dir_path_str)
1392
+ for module_path_str, file_path in shared_files.items():
1393
+ # Convert module path to Path object (e.g., "tools/weather/helpers" -> Path("tools/weather/helpers"))
1394
+ module_path = Path(module_path_str)
1463
1395
 
1464
1396
  # Get the component type (tools, resources, prompts)
1465
1397
  component_type = None
1466
- for part in dir_path.parts:
1398
+ for part in module_path.parts:
1467
1399
  if part in ["tools", "resources", "prompts"]:
1468
1400
  component_type = part
1469
1401
  break
@@ -1473,23 +1405,32 @@ def build_import_map(
1473
1405
 
1474
1406
  # Calculate the relative path within the component type
1475
1407
  try:
1476
- rel_to_component = dir_path.relative_to(component_type)
1408
+ rel_to_component = module_path.relative_to(component_type)
1477
1409
  # Create the new import path
1478
1410
  if str(rel_to_component) == ".":
1479
- # This is at the root of the component type
1411
+ # This shouldn't happen for individual files, but handle it
1480
1412
  new_path = f"components.{component_type}"
1481
1413
  else:
1482
1414
  # Replace path separators with dots
1483
1415
  path_parts = str(rel_to_component).replace("\\", "/").split("/")
1484
1416
  new_path = f"components.{component_type}.{'.'.join(path_parts)}"
1485
1417
 
1486
- # Map both the directory and the common file
1487
- orig_module = dir_path_str
1488
- import_map[orig_module] = new_path
1418
+ # Map the specific shared module
1419
+ # e.g., "tools/weather/helpers" -> "components.tools.weather.helpers"
1420
+ import_map[module_path_str] = new_path
1421
+
1422
+ # Also map the directory path for relative imports
1423
+ # e.g., "tools/weather" -> "components.tools.weather"
1424
+ dir_path_str = str(module_path.parent)
1425
+ if dir_path_str != "." and dir_path_str not in import_map:
1426
+ dir_rel_to_component = module_path.parent.relative_to(component_type)
1427
+ if str(dir_rel_to_component) == ".":
1428
+ dir_new_path = f"components.{component_type}"
1429
+ else:
1430
+ dir_path_parts = str(dir_rel_to_component).replace("\\", "/").split("/")
1431
+ dir_new_path = f"components.{component_type}.{'.'.join(dir_path_parts)}"
1432
+ import_map[dir_path_str] = dir_new_path
1489
1433
 
1490
- # Also map the specific common module
1491
- common_module = f"{dir_path_str}/common"
1492
- import_map[common_module] = f"{new_path}.common"
1493
1434
  except ValueError:
1494
1435
  continue
1495
1436