golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__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 (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +209 -87
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +81 -53
  12. golf/commands/run.py +30 -15
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +493 -362
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +216 -95
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +455 -310
  42. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.13.dist-info/RECORD +55 -0
  44. golf_mcp-0.1.11.dist-info/RECORD +0 -55
  45. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
  46. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
golf/core/builder.py CHANGED
@@ -5,80 +5,80 @@ import os
5
5
  import shutil
6
6
  import sys
7
7
  from pathlib import Path
8
- from typing import Any, Dict, List, Optional, Set
8
+ from typing import Any
9
9
 
10
10
  import black
11
11
  from rich.console import Console
12
12
  from rich.progress import Progress, SpinnerColumn, TextColumn
13
13
 
14
- from golf.core.config import Settings
15
- from golf.core.parser import (
16
- ComponentType,
17
- ParsedComponent,
18
- parse_project,
19
- )
20
- from golf.core.transformer import transform_component
21
- from golf.core.builder_auth import generate_auth_code, generate_auth_routes
22
14
  from golf.auth import get_auth_config
23
15
  from golf.auth.api_key import get_api_key_config
16
+ from golf.core.builder_auth import generate_auth_code, generate_auth_routes
24
17
  from golf.core.builder_telemetry import (
25
18
  generate_telemetry_imports,
26
- get_otel_dependencies
19
+ get_otel_dependencies,
20
+ )
21
+ from golf.core.config import Settings
22
+ from golf.core.parser import (
23
+ ComponentType,
24
+ ParsedComponent,
25
+ parse_project,
27
26
  )
27
+ from golf.core.transformer import transform_component
28
28
 
29
29
  console = Console()
30
30
 
31
31
 
32
32
  class ManifestBuilder:
33
33
  """Builds FastMCP manifest from parsed components."""
34
-
35
- def __init__(self, project_path: Path, settings: Settings):
34
+
35
+ def __init__(self, project_path: Path, settings: Settings) -> None:
36
36
  """Initialize the manifest builder.
37
-
37
+
38
38
  Args:
39
39
  project_path: Path to the project root
40
40
  settings: Project settings
41
41
  """
42
42
  self.project_path = project_path
43
43
  self.settings = settings
44
- self.components: Dict[ComponentType, List[ParsedComponent]] = {}
45
- self.manifest: Dict[str, Any] = {
44
+ self.components: dict[ComponentType, list[ParsedComponent]] = {}
45
+ self.manifest: dict[str, Any] = {
46
46
  "name": settings.name,
47
47
  "description": settings.description or "",
48
48
  "tools": [],
49
49
  "resources": [],
50
- "prompts": []
50
+ "prompts": [],
51
51
  }
52
-
53
- def build(self) -> Dict[str, Any]:
52
+
53
+ def build(self) -> dict[str, Any]:
54
54
  """Build the complete manifest.
55
-
55
+
56
56
  Returns:
57
57
  FastMCP manifest dictionary
58
58
  """
59
59
  # Parse all components
60
60
  self.components = parse_project(self.project_path)
61
-
61
+
62
62
  # Process each component type
63
63
  self._process_tools()
64
64
  self._process_resources()
65
65
  self._process_prompts()
66
-
66
+
67
67
  return self.manifest
68
-
68
+
69
69
  def _process_tools(self) -> None:
70
70
  """Process all tool components and add them to the manifest."""
71
71
  for component in self.components[ComponentType.TOOL]:
72
72
  # Extract the properties directly from the Input schema if it exists
73
73
  input_properties = {}
74
74
  required_fields = []
75
-
75
+
76
76
  if component.input_schema and "properties" in component.input_schema:
77
77
  input_properties = component.input_schema["properties"]
78
78
  # Get required fields if they exist
79
79
  if "required" in component.input_schema:
80
80
  required_fields = component.input_schema["required"]
81
-
81
+
82
82
  # Create a flattened tool schema matching FastMCP documentation examples
83
83
  tool_schema = {
84
84
  "name": component.name,
@@ -87,38 +87,38 @@ class ManifestBuilder:
87
87
  "type": "object",
88
88
  "properties": input_properties,
89
89
  "additionalProperties": False,
90
- "$schema": "http://json-schema.org/draft-07/schema#"
90
+ "$schema": "http://json-schema.org/draft-07/schema#",
91
91
  },
92
- "annotations": {
93
- "title": component.name.replace('-', ' ').title()
94
- },
95
- "entry_function": component.entry_function
92
+ "annotations": {"title": component.name.replace("-", " ").title()},
93
+ "entry_function": component.entry_function,
96
94
  }
97
-
95
+
98
96
  # Include required fields if they exist
99
97
  if required_fields:
100
98
  tool_schema["inputSchema"]["required"] = required_fields
101
-
99
+
102
100
  # Add the tool to the manifest
103
101
  self.manifest["tools"].append(tool_schema)
104
-
102
+
105
103
  def _process_resources(self) -> None:
106
104
  """Process all resource components and add them to the manifest."""
107
105
  for component in self.components[ComponentType.RESOURCE]:
108
106
  if not component.uri_template:
109
- console.print(f"[yellow]Warning: Resource {component.name} has no URI template[/yellow]")
107
+ console.print(
108
+ f"[yellow]Warning: Resource {component.name} has no URI template[/yellow]"
109
+ )
110
110
  continue
111
-
111
+
112
112
  resource_schema = {
113
113
  "uri": component.uri_template,
114
114
  "name": component.name,
115
115
  "description": component.docstring or "",
116
- "entry_function": component.entry_function
116
+ "entry_function": component.entry_function,
117
117
  }
118
-
118
+
119
119
  # Add the resource to the manifest
120
120
  self.manifest["resources"].append(resource_schema)
121
-
121
+
122
122
  def _process_prompts(self) -> None:
123
123
  """Process all prompt components and add them to the manifest."""
124
124
  for component in self.components[ComponentType.PROMPT]:
@@ -127,28 +127,27 @@ class ManifestBuilder:
127
127
  prompt_schema = {
128
128
  "name": component.name,
129
129
  "description": component.docstring or "",
130
- "entry_function": component.entry_function
130
+ "entry_function": component.entry_function,
131
131
  }
132
-
132
+
133
133
  # If the prompt has parameters, include them
134
134
  if component.parameters:
135
135
  arguments = []
136
136
  for param in component.parameters:
137
- arguments.append({
138
- "name": param,
139
- "required": True # Default to required
140
- })
137
+ arguments.append(
138
+ {"name": param, "required": True} # Default to required
139
+ )
141
140
  prompt_schema["arguments"] = arguments
142
-
141
+
143
142
  # Add the prompt to the manifest
144
143
  self.manifest["prompts"].append(prompt_schema)
145
-
146
- def save_manifest(self, output_path: Optional[Path] = None) -> Path:
144
+
145
+ def save_manifest(self, output_path: Path | None = None) -> Path:
147
146
  """Save the manifest to a JSON file.
148
-
147
+
149
148
  Args:
150
149
  output_path: Path to save the manifest to (defaults to .golf/manifest.json)
151
-
150
+
152
151
  Returns:
153
152
  Path where the manifest was saved
154
153
  """
@@ -157,25 +156,25 @@ class ManifestBuilder:
157
156
  golf_dir = self.project_path / ".golf"
158
157
  golf_dir.mkdir(exist_ok=True)
159
158
  output_path = golf_dir / "manifest.json"
160
-
159
+
161
160
  # Ensure parent directories exist
162
161
  output_path.parent.mkdir(parents=True, exist_ok=True)
163
-
162
+
164
163
  # Write the manifest to the file
165
164
  with open(output_path, "w") as f:
166
165
  json.dump(self.manifest, f, indent=2)
167
-
166
+
168
167
  console.print(f"[green]Manifest saved to {output_path}[/green]")
169
168
  return output_path
170
169
 
171
170
 
172
- def build_manifest(project_path: Path, settings: Settings) -> Dict[str, Any]:
171
+ def build_manifest(project_path: Path, settings: Settings) -> dict[str, Any]:
173
172
  """Build a FastMCP manifest from parsed components.
174
-
173
+
175
174
  Args:
176
175
  project_path: Path to the project root
177
176
  settings: Project settings
178
-
177
+
179
178
  Returns:
180
179
  FastMCP manifest dictionary
181
180
  """
@@ -185,90 +184,99 @@ def build_manifest(project_path: Path, settings: Settings) -> Dict[str, Any]:
185
184
 
186
185
 
187
186
  def compute_manifest_diff(
188
- old_manifest: Dict[str, Any], new_manifest: Dict[str, Any]
189
- ) -> Dict[str, Any]:
187
+ old_manifest: dict[str, Any], new_manifest: dict[str, Any]
188
+ ) -> dict[str, Any]:
190
189
  """Compute the difference between two manifests.
191
-
190
+
192
191
  Args:
193
192
  old_manifest: Previous manifest
194
193
  new_manifest: New manifest
195
-
194
+
196
195
  Returns:
197
196
  Dictionary describing the changes
198
197
  """
199
198
  diff = {
200
- "tools": {
201
- "added": [],
202
- "removed": [],
203
- "changed": []
204
- },
205
- "resources": {
206
- "added": [],
207
- "removed": [],
208
- "changed": []
209
- },
210
- "prompts": {
211
- "added": [],
212
- "removed": [],
213
- "changed": []
214
- }
199
+ "tools": {"added": [], "removed": [], "changed": []},
200
+ "resources": {"added": [], "removed": [], "changed": []},
201
+ "prompts": {"added": [], "removed": [], "changed": []},
215
202
  }
216
-
203
+
217
204
  # Helper function to extract names from a list of components
218
- def extract_names(components: List[Dict[str, Any]]) -> Set[str]:
205
+ def extract_names(components: list[dict[str, Any]]) -> set[str]:
219
206
  return {comp["name"] for comp in components}
220
-
207
+
221
208
  # Compare tools
222
209
  old_tools = extract_names(old_manifest.get("tools", []))
223
210
  new_tools = extract_names(new_manifest.get("tools", []))
224
211
  diff["tools"]["added"] = list(new_tools - old_tools)
225
212
  diff["tools"]["removed"] = list(old_tools - new_tools)
226
-
213
+
227
214
  # Compare tools that exist in both for changes
228
215
  for new_tool in new_manifest.get("tools", []):
229
216
  if new_tool["name"] in old_tools:
230
217
  # Find the corresponding old tool
231
- old_tool = next((t for t in old_manifest.get("tools", []) if t["name"] == new_tool["name"]), None)
218
+ old_tool = next(
219
+ (
220
+ t
221
+ for t in old_manifest.get("tools", [])
222
+ if t["name"] == new_tool["name"]
223
+ ),
224
+ None,
225
+ )
232
226
  if old_tool and json.dumps(old_tool) != json.dumps(new_tool):
233
227
  diff["tools"]["changed"].append(new_tool["name"])
234
-
228
+
235
229
  # Compare resources
236
230
  old_resources = extract_names(old_manifest.get("resources", []))
237
231
  new_resources = extract_names(new_manifest.get("resources", []))
238
232
  diff["resources"]["added"] = list(new_resources - old_resources)
239
233
  diff["resources"]["removed"] = list(old_resources - new_resources)
240
-
234
+
241
235
  # Compare resources that exist in both for changes
242
236
  for new_resource in new_manifest.get("resources", []):
243
237
  if new_resource["name"] in old_resources:
244
238
  # Find the corresponding old resource
245
- old_resource = next((r for r in old_manifest.get("resources", []) if r["name"] == new_resource["name"]), None)
239
+ old_resource = next(
240
+ (
241
+ r
242
+ for r in old_manifest.get("resources", [])
243
+ if r["name"] == new_resource["name"]
244
+ ),
245
+ None,
246
+ )
246
247
  if old_resource and json.dumps(old_resource) != json.dumps(new_resource):
247
248
  diff["resources"]["changed"].append(new_resource["name"])
248
-
249
+
249
250
  # Compare prompts
250
251
  old_prompts = extract_names(old_manifest.get("prompts", []))
251
252
  new_prompts = extract_names(new_manifest.get("prompts", []))
252
253
  diff["prompts"]["added"] = list(new_prompts - old_prompts)
253
254
  diff["prompts"]["removed"] = list(old_prompts - new_prompts)
254
-
255
+
255
256
  # Compare prompts that exist in both for changes
256
257
  for new_prompt in new_manifest.get("prompts", []):
257
258
  if new_prompt["name"] in old_prompts:
258
259
  # Find the corresponding old prompt
259
- old_prompt = next((p for p in old_manifest.get("prompts", []) if p["name"] == new_prompt["name"]), None)
260
+ old_prompt = next(
261
+ (
262
+ p
263
+ for p in old_manifest.get("prompts", [])
264
+ if p["name"] == new_prompt["name"]
265
+ ),
266
+ None,
267
+ )
260
268
  if old_prompt and json.dumps(old_prompt) != json.dumps(new_prompt):
261
269
  diff["prompts"]["changed"].append(new_prompt["name"])
262
-
270
+
263
271
  return diff
264
272
 
265
273
 
266
- def has_changes(diff: Dict[str, Any]) -> bool:
274
+ def has_changes(diff: dict[str, Any]) -> bool:
267
275
  """Check if a manifest diff contains any changes.
268
-
276
+
269
277
  Args:
270
278
  diff: Manifest diff from compute_manifest_diff
271
-
279
+
272
280
  Returns:
273
281
  True if there are any changes, False otherwise
274
282
  """
@@ -276,16 +284,23 @@ def has_changes(diff: Dict[str, Any]) -> bool:
276
284
  for change_type in diff[category]:
277
285
  if diff[category][change_type]:
278
286
  return True
279
-
287
+
280
288
  return False
281
289
 
282
290
 
283
291
  class CodeGenerator:
284
292
  """Code generator for FastMCP applications."""
285
-
286
- def __init__(self, project_path: Path, settings: Settings, output_dir: Path, build_env: str = "prod", copy_env: bool = False):
293
+
294
+ def __init__(
295
+ self,
296
+ project_path: Path,
297
+ settings: Settings,
298
+ output_dir: Path,
299
+ build_env: str = "prod",
300
+ copy_env: bool = False,
301
+ ) -> None:
287
302
  """Initialize the code generator.
288
-
303
+
289
304
  Args:
290
305
  project_path: Path to the project root
291
306
  settings: Project settings
@@ -302,22 +317,22 @@ class CodeGenerator:
302
317
  self.manifest = {}
303
318
  self.common_files = {}
304
319
  self.import_map = {}
305
-
320
+
306
321
  def generate(self) -> None:
307
322
  """Generate the FastMCP application code."""
308
323
  # Parse the project and build the manifest
309
324
  with console.status("Analyzing project components..."):
310
325
  self.components = parse_project(self.project_path)
311
326
  self.manifest = build_manifest(self.project_path, self.settings)
312
-
327
+
313
328
  # Find common.py files and build import map
314
329
  self.common_files = find_common_files(self.project_path, self.components)
315
330
  self.import_map = build_import_map(self.project_path, self.common_files)
316
-
331
+
317
332
  # Create output directory structure
318
333
  with console.status("Creating directory structure..."):
319
334
  self._create_directory_structure()
320
-
335
+
321
336
  # Generate code for all components
322
337
  with Progress(
323
338
  SpinnerColumn(),
@@ -330,21 +345,23 @@ class CodeGenerator:
330
345
  ("prompts", self._generate_prompts),
331
346
  ("server entry point", self._generate_server),
332
347
  ]
333
-
348
+
334
349
  for description, func in tasks:
335
350
  task = progress.add_task(description, total=1)
336
351
  func()
337
352
  progress.update(task, completed=1)
338
-
353
+
339
354
  # Get relative path for display
340
355
  try:
341
356
  output_dir_display = self.output_dir.relative_to(Path.cwd())
342
357
  except ValueError:
343
358
  output_dir_display = self.output_dir
344
-
359
+
345
360
  # Show success message with output directory
346
- console.print(f"[bold green]✓[/bold green] Build completed successfully in [bold]{output_dir_display}[/bold]")
347
-
361
+ console.print(
362
+ f"[bold green]✓[/bold green] Build completed successfully in [bold]{output_dir_display}[/bold]"
363
+ )
364
+
348
365
  def _create_directory_structure(self) -> None:
349
366
  """Create the output directory structure"""
350
367
  # Create main directories
@@ -355,162 +372,157 @@ class CodeGenerator:
355
372
  self.output_dir / "components" / "resources",
356
373
  self.output_dir / "components" / "prompts",
357
374
  ]
358
-
375
+
359
376
  for directory in dirs:
360
- directory.mkdir(parents=True, exist_ok=True)
377
+ directory.mkdir(parents=True, exist_ok=True)
361
378
  # Process common.py files directly in the components directory
362
379
  self._process_common_files()
363
-
380
+
364
381
  def _process_common_files(self) -> None:
365
382
  """Process and transform common.py files in the components directory structure."""
366
383
  # Reuse the already fetched common_files instead of calling the function again
367
384
  for dir_path_str, common_file in self.common_files.items():
368
385
  # Convert string path to Path object
369
386
  dir_path = Path(dir_path_str)
370
-
387
+
371
388
  # Determine the component type
372
389
  component_type = None
373
390
  for part in dir_path.parts:
374
391
  if part in ["tools", "resources", "prompts"]:
375
392
  component_type = part
376
393
  break
377
-
394
+
378
395
  if not component_type:
379
396
  continue
380
-
397
+
381
398
  # Calculate target directory in components structure
382
399
  rel_to_component = dir_path.relative_to(component_type)
383
- target_dir = self.output_dir / "components" / component_type / rel_to_component
384
-
400
+ target_dir = (
401
+ self.output_dir / "components" / component_type / rel_to_component
402
+ )
403
+
385
404
  # Create directory if it doesn't exist
386
405
  target_dir.mkdir(parents=True, exist_ok=True)
387
-
406
+
388
407
  # Create the common.py file in the target directory
389
408
  target_file = target_dir / "common.py"
390
-
409
+
391
410
  # Use transformer to process the file
392
411
  transform_component(
393
412
  component=None,
394
413
  output_file=target_file,
395
414
  project_path=self.project_path,
396
415
  import_map=self.import_map,
397
- source_file=common_file
416
+ source_file=common_file,
398
417
  )
399
-
418
+
400
419
  def _generate_tools(self) -> None:
401
420
  """Generate code for all tools."""
402
421
  tools_dir = self.output_dir / "components" / "tools"
403
-
422
+
404
423
  for tool in self.components.get(ComponentType.TOOL, []):
405
424
  # Get the tool directory structure
406
425
  rel_path = Path(tool.file_path).relative_to(self.project_path)
407
426
  if not rel_path.is_relative_to(Path(self.settings.tools_dir)):
408
- console.print(f"[yellow]Warning: Tool {tool.name} is not in the tools directory[/yellow]")
427
+ console.print(
428
+ f"[yellow]Warning: Tool {tool.name} is not in the tools directory[/yellow]"
429
+ )
409
430
  continue
410
-
431
+
411
432
  try:
412
433
  rel_to_tools = rel_path.relative_to(self.settings.tools_dir)
413
434
  tool_dir = tools_dir / rel_to_tools.parent
414
435
  except ValueError:
415
436
  # Fall back to just using the filename
416
437
  tool_dir = tools_dir
417
-
438
+
418
439
  tool_dir.mkdir(parents=True, exist_ok=True)
419
-
440
+
420
441
  # Create the tool file
421
442
  output_file = tool_dir / rel_path.name
422
- transform_component(
423
- tool,
424
- output_file,
425
- self.project_path,
426
- self.import_map
427
- )
428
-
443
+ transform_component(tool, output_file, self.project_path, self.import_map)
444
+
429
445
  def _generate_resources(self) -> None:
430
446
  """Generate code for all resources."""
431
447
  resources_dir = self.output_dir / "components" / "resources"
432
-
448
+
433
449
  for resource in self.components.get(ComponentType.RESOURCE, []):
434
450
  # Get the resource directory structure
435
451
  rel_path = Path(resource.file_path).relative_to(self.project_path)
436
452
  if not rel_path.is_relative_to(Path(self.settings.resources_dir)):
437
- console.print(f"[yellow]Warning: Resource {resource.name} is not in the resources directory[/yellow]")
453
+ console.print(
454
+ f"[yellow]Warning: Resource {resource.name} is not in the resources directory[/yellow]"
455
+ )
438
456
  continue
439
-
457
+
440
458
  try:
441
459
  rel_to_resources = rel_path.relative_to(self.settings.resources_dir)
442
460
  resource_dir = resources_dir / rel_to_resources.parent
443
461
  except ValueError:
444
462
  # Fall back to just using the filename
445
463
  resource_dir = resources_dir
446
-
464
+
447
465
  resource_dir.mkdir(parents=True, exist_ok=True)
448
-
466
+
449
467
  # Create the resource file
450
468
  output_file = resource_dir / rel_path.name
451
469
  transform_component(
452
- resource,
453
- output_file,
454
- self.project_path,
455
- self.import_map
470
+ resource, output_file, self.project_path, self.import_map
456
471
  )
457
-
472
+
458
473
  def _generate_prompts(self) -> None:
459
474
  """Generate code for all prompts."""
460
475
  prompts_dir = self.output_dir / "components" / "prompts"
461
-
476
+
462
477
  for prompt in self.components.get(ComponentType.PROMPT, []):
463
478
  # Get the prompt directory structure
464
479
  rel_path = Path(prompt.file_path).relative_to(self.project_path)
465
480
  if not rel_path.is_relative_to(Path(self.settings.prompts_dir)):
466
- console.print(f"[yellow]Warning: Prompt {prompt.name} is not in the prompts directory[/yellow]")
481
+ console.print(
482
+ f"[yellow]Warning: Prompt {prompt.name} is not in the prompts directory[/yellow]"
483
+ )
467
484
  continue
468
-
485
+
469
486
  try:
470
487
  rel_to_prompts = rel_path.relative_to(self.settings.prompts_dir)
471
488
  prompt_dir = prompts_dir / rel_to_prompts.parent
472
489
  except ValueError:
473
490
  # Fall back to just using the filename
474
491
  prompt_dir = prompts_dir
475
-
492
+
476
493
  prompt_dir.mkdir(parents=True, exist_ok=True)
477
-
494
+
478
495
  # Create the prompt file
479
496
  output_file = prompt_dir / rel_path.name
480
- transform_component(
481
- prompt,
482
- output_file,
483
- self.project_path,
484
- self.import_map
485
- )
486
-
497
+ transform_component(prompt, output_file, self.project_path, self.import_map)
498
+
487
499
  def _get_transport_config(self, transport_type: str) -> dict:
488
500
  """Get transport-specific configuration (primarily for endpoint path display).
489
-
501
+
490
502
  Args:
491
503
  transport_type: The transport type (e.g., 'sse', 'streamable-http', 'stdio')
492
-
504
+
493
505
  Returns:
494
506
  Dictionary with transport configuration details (endpoint_path)
495
507
  """
496
508
  config = {
497
509
  "endpoint_path": "",
498
510
  }
499
-
511
+
500
512
  if transport_type == "sse":
501
- config["endpoint_path"] = "/sse" # Default SSE path for FastMCP
513
+ config["endpoint_path"] = "/sse" # Default SSE path for FastMCP
502
514
  elif transport_type == "stdio":
503
- config["endpoint_path"] = "" # No HTTP endpoint
515
+ config["endpoint_path"] = "" # No HTTP endpoint
504
516
  else:
505
517
  # Default to streamable-http
506
- config["endpoint_path"] = "/mcp" # Default MCP path for FastMCP
507
-
518
+ config["endpoint_path"] = "/mcp" # Default MCP path for FastMCP
519
+
508
520
  return config
509
-
521
+
510
522
  def _generate_server(self) -> None:
511
523
  """Generate the main server entry point."""
512
524
  server_file = self.output_dir / "server.py"
513
-
525
+
514
526
  # Get auth components
515
527
  provider_config, _ = get_auth_config()
516
528
  auth_components = generate_auth_code(
@@ -519,9 +531,9 @@ class CodeGenerator:
519
531
  port=self.settings.port,
520
532
  https=False, # This could be configurable in settings
521
533
  opentelemetry_enabled=self.settings.opentelemetry_enabled,
522
- transport=self.settings.transport
534
+ transport=self.settings.transport,
523
535
  )
524
-
536
+
525
537
  # Create imports section
526
538
  imports = [
527
539
  "from fastmcp import FastMCP",
@@ -533,32 +545,33 @@ class CodeGenerator:
533
545
  "# Suppress FastMCP INFO logs",
534
546
  "logging.getLogger('fastmcp').setLevel(logging.WARNING)",
535
547
  "logging.getLogger('mcp').setLevel(logging.WARNING)",
536
- ""
548
+ "",
537
549
  ]
538
-
550
+
539
551
  # Add auth imports if auth is configured
540
552
  if auth_components.get("has_auth"):
541
553
  imports.extend(auth_components["imports"])
542
554
  imports.append("")
543
-
555
+
544
556
  # Add OpenTelemetry imports if enabled
545
557
  if self.settings.opentelemetry_enabled:
546
558
  imports.extend(generate_telemetry_imports())
547
559
  imports.append("")
548
-
560
+
549
561
  # Add imports section for different transport methods
550
- if self.settings.transport == "sse":
562
+ if self.settings.transport == "sse" or self.settings.transport in [
563
+ "streamable-http",
564
+ "http",
565
+ ]:
551
566
  imports.append("import uvicorn")
552
- elif self.settings.transport in ["streamable-http", "http"]:
553
- imports.append("import uvicorn")
554
-
567
+
555
568
  # Get transport-specific configuration
556
569
  transport_config = self._get_transport_config(self.settings.transport)
557
570
  endpoint_path = transport_config["endpoint_path"]
558
-
571
+
559
572
  # Track component modules to register
560
573
  component_registrations = []
561
-
574
+
562
575
  # Import components
563
576
  for component_type in self.components:
564
577
  # Add a section header
@@ -571,20 +584,24 @@ class CodeGenerator:
571
584
  else:
572
585
  imports.append("# Import prompts")
573
586
  comp_section = "# Register prompts"
574
-
587
+
575
588
  component_registrations.append(comp_section)
576
-
589
+
577
590
  for component in self.components[component_type]:
578
591
  # Derive the import path based on component type and file path
579
592
  rel_path = Path(component.file_path).relative_to(self.project_path)
580
593
  module_name = rel_path.stem
581
-
594
+
582
595
  if component_type == ComponentType.TOOL:
583
596
  try:
584
597
  rel_to_tools = rel_path.relative_to(self.settings.tools_dir)
585
598
  # Handle nested directories properly
586
599
  if rel_to_tools.parent != Path("."):
587
- parent_path = str(rel_to_tools.parent).replace("\\", ".").replace("/", ".")
600
+ parent_path = (
601
+ str(rel_to_tools.parent)
602
+ .replace("\\", ".")
603
+ .replace("/", ".")
604
+ )
588
605
  import_path = f"components.tools.{parent_path}"
589
606
  else:
590
607
  import_path = "components.tools"
@@ -592,10 +609,16 @@ class CodeGenerator:
592
609
  import_path = "components.tools"
593
610
  elif component_type == ComponentType.RESOURCE:
594
611
  try:
595
- rel_to_resources = rel_path.relative_to(self.settings.resources_dir)
612
+ rel_to_resources = rel_path.relative_to(
613
+ self.settings.resources_dir
614
+ )
596
615
  # Handle nested directories properly
597
616
  if rel_to_resources.parent != Path("."):
598
- parent_path = str(rel_to_resources.parent).replace("\\", ".").replace("/", ".")
617
+ parent_path = (
618
+ str(rel_to_resources.parent)
619
+ .replace("\\", ".")
620
+ .replace("/", ".")
621
+ )
599
622
  import_path = f"components.resources.{parent_path}"
600
623
  else:
601
624
  import_path = "components.resources"
@@ -606,107 +629,127 @@ class CodeGenerator:
606
629
  rel_to_prompts = rel_path.relative_to(self.settings.prompts_dir)
607
630
  # Handle nested directories properly
608
631
  if rel_to_prompts.parent != Path("."):
609
- parent_path = str(rel_to_prompts.parent).replace("\\", ".").replace("/", ".")
632
+ parent_path = (
633
+ str(rel_to_prompts.parent)
634
+ .replace("\\", ".")
635
+ .replace("/", ".")
636
+ )
610
637
  import_path = f"components.prompts.{parent_path}"
611
638
  else:
612
639
  import_path = "components.prompts"
613
640
  except ValueError:
614
641
  import_path = "components.prompts"
615
-
642
+
616
643
  # Clean up the import path
617
644
  import_path = import_path.rstrip(".")
618
-
645
+
619
646
  # Add the import for the component's module
620
647
  full_module_path = f"{import_path}.{module_name}"
621
648
  imports.append(f"import {full_module_path}")
622
-
649
+
623
650
  # Add code to register this component
624
651
  if self.settings.opentelemetry_enabled:
625
652
  # Use telemetry instrumentation
626
653
  registration = f"# Register the {component_type.value} '{component.name}' with telemetry"
627
- entry_func = component.entry_function if hasattr(component, "entry_function") and component.entry_function else "export"
628
-
654
+ entry_func = (
655
+ component.entry_function
656
+ if hasattr(component, "entry_function")
657
+ and component.entry_function
658
+ else "export"
659
+ )
660
+
629
661
  # Debug: Add logging to verify wrapping
630
662
  registration += f"\n_wrapped_func = instrument_{component_type.value}({full_module_path}.{entry_func}, '{component.name}')"
631
-
663
+
632
664
  if component_type == ComponentType.TOOL:
633
- registration += f"\nmcp.add_tool(_wrapped_func, name=\"{component.name}\", description=\"{component.docstring or ''}\")"
665
+ registration += f'\nmcp.add_tool(_wrapped_func, name="{component.name}", description="{component.docstring or ""}")'
634
666
  elif component_type == ComponentType.RESOURCE:
635
- registration += f"\nmcp.add_resource_fn(_wrapped_func, uri=\"{component.uri_template}\", name=\"{component.name}\", description=\"{component.docstring or ''}\")"
667
+ registration += f'\nmcp.add_resource_fn(_wrapped_func, uri="{component.uri_template}", name="{component.name}", description="{component.docstring or ""}")'
636
668
  else: # PROMPT
637
- registration += f"\nmcp.add_prompt(_wrapped_func, name=\"{component.name}\", description=\"{component.docstring or ''}\")"
669
+ registration += f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", description="{component.docstring or ""}")'
638
670
  else:
639
671
  # Standard registration without telemetry
640
672
  if component_type == ComponentType.TOOL:
641
673
  registration = f"# Register the tool '{component.name}' from {full_module_path}"
642
-
674
+
643
675
  # Use the entry_function if available, otherwise try the export variable
644
- if hasattr(component, "entry_function") and component.entry_function:
676
+ if (
677
+ hasattr(component, "entry_function")
678
+ and component.entry_function
679
+ ):
645
680
  registration += f"\nmcp.add_tool({full_module_path}.{component.entry_function}"
646
681
  else:
647
682
  registration += f"\nmcp.add_tool({full_module_path}.export"
648
-
683
+
649
684
  # Add the name parameter
650
- registration += f", name=\"{component.name}\""
651
-
685
+ registration += f', name="{component.name}"'
686
+
652
687
  # Add description from docstring
653
688
  if component.docstring:
654
689
  # Escape any quotes in the docstring
655
- escaped_docstring = component.docstring.replace("\"", "\\\"")
656
- registration += f", description=\"{escaped_docstring}\""
690
+ escaped_docstring = component.docstring.replace('"', '\\"')
691
+ registration += f', description="{escaped_docstring}"'
657
692
  registration += ")"
658
693
 
659
694
  elif component_type == ComponentType.RESOURCE:
660
695
  registration = f"# Register the resource '{component.name}' from {full_module_path}"
661
-
696
+
662
697
  # Use the entry_function if available, otherwise try the export variable
663
- if hasattr(component, "entry_function") and component.entry_function:
664
- registration += f"\nmcp.add_resource_fn({full_module_path}.{component.entry_function}, uri=\"{component.uri_template}\""
698
+ if (
699
+ hasattr(component, "entry_function")
700
+ and component.entry_function
701
+ ):
702
+ registration += f'\nmcp.add_resource_fn({full_module_path}.{component.entry_function}, uri="{component.uri_template}"'
665
703
  else:
666
- registration += f"\nmcp.add_resource_fn({full_module_path}.export, uri=\"{component.uri_template}\""
667
-
704
+ registration += f'\nmcp.add_resource_fn({full_module_path}.export, uri="{component.uri_template}"'
705
+
668
706
  # Add the name parameter
669
- registration += f", name=\"{component.name}\""
670
-
707
+ registration += f', name="{component.name}"'
708
+
671
709
  # Add description from docstring
672
710
  if component.docstring:
673
711
  # Escape any quotes in the docstring
674
- escaped_docstring = component.docstring.replace("\"", "\\\"")
675
- registration += f", description=\"{escaped_docstring}\""
712
+ escaped_docstring = component.docstring.replace('"', '\\"')
713
+ registration += f', description="{escaped_docstring}"'
676
714
  registration += ")"
677
715
 
678
716
  else: # PROMPT
679
717
  registration = f"# Register the prompt '{component.name}' from {full_module_path}"
680
-
718
+
681
719
  # Use the entry_function if available, otherwise try the export variable
682
- if hasattr(component, "entry_function") and component.entry_function:
720
+ if (
721
+ hasattr(component, "entry_function")
722
+ and component.entry_function
723
+ ):
683
724
  registration += f"\nmcp.add_prompt({full_module_path}.{component.entry_function}"
684
725
  else:
685
- registration += f"\nmcp.add_prompt({full_module_path}.export"
686
-
726
+ registration += (
727
+ f"\nmcp.add_prompt({full_module_path}.export"
728
+ )
729
+
687
730
  # Add the name parameter
688
- registration += f", name=\"{component.name}\""
689
-
731
+ registration += f', name="{component.name}"'
732
+
690
733
  # Add description from docstring
691
734
  if component.docstring:
692
735
  # Escape any quotes in the docstring
693
- escaped_docstring = component.docstring.replace("\"", "\\\"")
694
- registration += f", description=\"{escaped_docstring}\""
736
+ escaped_docstring = component.docstring.replace('"', '\\"')
737
+ registration += f', description="{escaped_docstring}"'
695
738
  registration += ")"
696
-
739
+
697
740
  component_registrations.append(registration)
698
-
741
+
699
742
  # Add a blank line after each section
700
743
  imports.append("")
701
744
  component_registrations.append("")
702
-
745
+
703
746
  # Create environment section based on build type - moved after imports
704
747
  env_section = [
705
748
  "",
706
749
  "# Load environment variables from .env file if it exists",
707
750
  "# Note: dotenv will not override existing environment variables by default",
708
751
  "load_dotenv()",
709
- ""
752
+ "",
710
753
  ]
711
754
 
712
755
  # OpenTelemetry setup code will be handled through imports and lifespan
@@ -718,15 +761,15 @@ class CodeGenerator:
718
761
 
719
762
  # Create FastMCP instance section
720
763
  server_code_lines = ["# Create FastMCP server"]
721
-
764
+
722
765
  # Build FastMCP constructor arguments
723
766
  mcp_constructor_args = [f'"{self.settings.name}"']
724
-
767
+
725
768
  # Add auth arguments if configured
726
769
  if auth_components.get("has_auth") and auth_components.get("fastmcp_args"):
727
770
  for key, value in auth_components["fastmcp_args"].items():
728
771
  mcp_constructor_args.append(f"{key}={value}")
729
-
772
+
730
773
  # Add OpenTelemetry parameters if enabled
731
774
  if self.settings.opentelemetry_enabled:
732
775
  mcp_constructor_args.append("lifespan=telemetry_lifespan")
@@ -734,120 +777,137 @@ class CodeGenerator:
734
777
  mcp_instance_line = f"mcp = FastMCP({', '.join(mcp_constructor_args)})"
735
778
  server_code_lines.append(mcp_instance_line)
736
779
  server_code_lines.append("")
737
-
780
+
738
781
  # Main entry point with transport-specific app initialization
739
782
  main_code = [
740
- "if __name__ == \"__main__\":",
783
+ 'if __name__ == "__main__":',
741
784
  " from rich.console import Console",
742
785
  " from rich.panel import Panel",
743
786
  " console = Console()",
744
787
  " # Get configuration from environment variables or use defaults",
745
- " host = os.environ.get(\"HOST\", \"127.0.0.1\")",
746
- " port = int(os.environ.get(\"PORT\", 3000))",
747
- f" transport_to_run = \"{self.settings.transport}\"",
748
- ""
788
+ ' host = os.environ.get("HOST", "127.0.0.1")',
789
+ ' port = int(os.environ.get("PORT", 3000))',
790
+ f' transport_to_run = "{self.settings.transport}"',
791
+ "",
749
792
  ]
750
-
793
+
751
794
  # Add startup message
752
795
  if self.settings.transport != "stdio":
753
- main_code.append(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"))')
796
+ main_code.append(
797
+ 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"))'
798
+ )
754
799
  else:
755
- main_code.append(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"))')
756
-
800
+ main_code.append(
801
+ 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"))'
802
+ )
803
+
757
804
  main_code.append("")
758
-
805
+
759
806
  # Transport-specific run methods
760
807
  if self.settings.transport == "sse":
761
808
  # Check if we need to add API key middleware for SSE
762
809
  api_key_config = get_api_key_config()
763
810
  if auth_components.get("has_auth") and api_key_config:
764
- main_code.extend([
765
- " # For SSE with API key auth, we need to get the app and add middleware",
766
- " app = mcp.http_app(transport=\"sse\")",
767
- " app.add_middleware(ApiKeyMiddleware)",
768
- ])
811
+ main_code.extend(
812
+ [
813
+ " # For SSE with API key auth, we need to get the app and add middleware",
814
+ ' app = mcp.http_app(transport="sse")',
815
+ " app.add_middleware(ApiKeyMiddleware)",
816
+ ]
817
+ )
769
818
  else:
770
- main_code.extend([
771
- " # For SSE, get the app to add middleware",
772
- " app = mcp.http_app(transport=\"sse\")",
773
- ])
774
-
819
+ main_code.extend(
820
+ [
821
+ " # For SSE, get the app to add middleware",
822
+ ' app = mcp.http_app(transport="sse")',
823
+ ]
824
+ )
825
+
775
826
  # Add OpenTelemetry middleware to the SSE app if enabled
776
827
  if self.settings.opentelemetry_enabled:
777
- main_code.extend([
778
- " # Apply OpenTelemetry middleware to the SSE app",
779
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
780
- " app = OpenTelemetryMiddleware(app)",
781
- ])
782
-
783
- main_code.extend([
784
- " # Run with the configured app",
785
- " uvicorn.run(app, host=host, port=port, log_level=\"info\")"
786
- ])
828
+ main_code.extend(
829
+ [
830
+ " # Apply OpenTelemetry middleware to the SSE app",
831
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
832
+ " app = OpenTelemetryMiddleware(app)",
833
+ ]
834
+ )
835
+
836
+ main_code.extend(
837
+ [
838
+ " # Run with the configured app",
839
+ ' uvicorn.run(app, host=host, port=port, log_level="info")',
840
+ ]
841
+ )
787
842
  elif self.settings.transport in ["streamable-http", "http"]:
788
- main_code.extend([
789
- " # Create HTTP app and run with uvicorn",
790
- " app = mcp.http_app()",
791
- ])
792
-
843
+ main_code.extend(
844
+ [
845
+ " # Create HTTP app and run with uvicorn",
846
+ " app = mcp.http_app()",
847
+ ]
848
+ )
849
+
793
850
  # Check if we need to add API key middleware
794
851
  api_key_config = get_api_key_config()
795
852
  if auth_components.get("has_auth") and api_key_config:
796
- main_code.extend([
797
- " # Add API key middleware",
798
- " app.add_middleware(ApiKeyMiddleware)",
799
- ])
800
-
853
+ main_code.extend(
854
+ [
855
+ " # Add API key middleware",
856
+ " app.add_middleware(ApiKeyMiddleware)",
857
+ ]
858
+ )
859
+
801
860
  # Add OpenTelemetry middleware to the HTTP app if enabled
802
861
  if self.settings.opentelemetry_enabled:
803
- main_code.extend([
804
- " # Apply OpenTelemetry middleware to the HTTP app",
805
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
806
- " app = OpenTelemetryMiddleware(app)",
807
- ])
808
-
809
- main_code.extend([
810
- " uvicorn.run(app, host=host, port=port, log_level=\"info\")"
811
- ])
862
+ main_code.extend(
863
+ [
864
+ " # Apply OpenTelemetry middleware to the HTTP app",
865
+ " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
866
+ " app = OpenTelemetryMiddleware(app)",
867
+ ]
868
+ )
869
+
870
+ main_code.extend(
871
+ [' uvicorn.run(app, host=host, port=port, log_level="info")']
872
+ )
812
873
  else:
813
874
  # For stdio transport, use mcp.run()
814
- main_code.extend([
815
- " # Run with stdio transport",
816
- " mcp.run(transport=\"stdio\")"
817
- ])
818
-
875
+ main_code.extend(
876
+ [" # Run with stdio transport", ' mcp.run(transport="stdio")']
877
+ )
878
+
819
879
  # Combine all sections
820
- # Order: imports, env_section, auth_setup, server_code (mcp init),
880
+ # Order: imports, env_section, auth_setup, server_code (mcp init),
821
881
  # post_init (API key middleware), component_registrations, main_code (run block)
822
882
  code = "\n".join(
823
- imports +
824
- env_section +
825
- auth_setup_code +
826
- server_code_lines +
827
- component_registrations +
828
- main_code
883
+ imports
884
+ + env_section
885
+ + auth_setup_code
886
+ + server_code_lines
887
+ + component_registrations
888
+ + main_code
829
889
  )
830
-
890
+
831
891
  # Format with black
832
892
  try:
833
893
  code = black.format_str(code, mode=black.Mode())
834
894
  except Exception as e:
835
895
  console.print(f"[yellow]Warning: Could not format server.py: {e}[/yellow]")
836
-
896
+
837
897
  # Write to file
838
898
  with open(server_file, "w") as f:
839
899
  f.write(code)
840
900
 
841
901
 
842
902
  def build_project(
843
- project_path: Path,
844
- settings: Settings,
903
+ project_path: Path,
904
+ settings: Settings,
845
905
  output_dir: Path,
846
906
  build_env: str = "prod",
847
- copy_env: bool = False
907
+ copy_env: bool = False,
848
908
  ) -> None:
849
909
  """Build a standalone FastMCP application from a GolfMCP project.
850
-
910
+
851
911
  Args:
852
912
  project_path: Path to the project directory
853
913
  settings: Project settings
@@ -862,38 +922,58 @@ def build_project(
862
922
  # Save the current directory and path
863
923
  original_dir = os.getcwd()
864
924
  original_path = sys.path.copy()
865
-
925
+
866
926
  # Change to the project directory and add it to Python path
867
927
  os.chdir(project_path)
868
928
  sys.path.insert(0, str(project_path))
869
-
929
+
870
930
  # Execute the pre_build script
871
931
  with open(pre_build_path) as f:
872
932
  script_content = f.read()
873
-
933
+
874
934
  # Print the first few lines for debugging
875
- preview = "\n".join(script_content.split("\n")[:5]) + "\n..."
876
-
935
+ "\n".join(script_content.split("\n")[:5]) + "\n..."
936
+
877
937
  # Use exec to run the script as a module
878
- code = compile(script_content, str(pre_build_path), 'exec')
938
+ code = compile(script_content, str(pre_build_path), "exec")
879
939
  exec(code, {})
880
-
940
+
881
941
  # Check if auth was configured by the script
882
942
  provider, scopes = get_auth_config()
883
-
943
+
884
944
  # Restore original directory and path
885
945
  os.chdir(original_dir)
886
946
  sys.path = original_path
887
-
947
+
888
948
  except Exception as e:
889
949
  console.print(f"[red]Error executing pre_build.py: {str(e)}[/red]")
890
950
  import traceback
951
+
891
952
  console.print(f"[red]{traceback.format_exc()}[/red]")
892
-
953
+
954
+ # Track detailed error for pre_build.py execution failures
955
+ try:
956
+ from golf.core.telemetry import track_detailed_error
957
+ track_detailed_error(
958
+ "build_pre_build_failed",
959
+ e,
960
+ context="Executing pre_build.py configuration script",
961
+ operation="pre_build_execution",
962
+ additional_props={
963
+ "file_path": str(pre_build_path.relative_to(project_path)),
964
+ "build_env": build_env,
965
+ }
966
+ )
967
+ except Exception:
968
+ # Don't let telemetry errors break the build
969
+ pass
970
+
893
971
  # Clear the output directory if it exists
894
972
  if output_dir.exists():
895
973
  shutil.rmtree(output_dir)
896
- output_dir.mkdir(parents=True, exist_ok=True) # Ensure output_dir exists after clearing
974
+ output_dir.mkdir(
975
+ parents=True, exist_ok=True
976
+ ) # Ensure output_dir exists after clearing
897
977
 
898
978
  # --- BEGIN Enhanced .env handling ---
899
979
  env_vars_to_write = {}
@@ -905,33 +985,46 @@ def build_project(
905
985
  if project_env_file.exists():
906
986
  try:
907
987
  from dotenv import dotenv_values
988
+
908
989
  env_vars_to_write.update(dotenv_values(project_env_file))
909
990
  except ImportError:
910
- console.print("[yellow]Warning: python-dotenv is not installed. Cannot read existing .env file for rich merging. Copying directly.[/yellow]")
991
+ console.print(
992
+ "[yellow]Warning: python-dotenv is not installed. Cannot read existing .env file for rich merging. Copying directly.[/yellow]"
993
+ )
911
994
  try:
912
995
  shutil.copy(project_env_file, env_file_path)
913
996
  # If direct copy happens, re-read for step 2 & 3 to respect its content
914
997
  if env_file_path.exists():
915
- from dotenv import dotenv_values
916
- env_vars_to_write.update(dotenv_values(env_file_path)) # Read what was copied
998
+ from dotenv import dotenv_values
999
+
1000
+ env_vars_to_write.update(
1001
+ dotenv_values(env_file_path)
1002
+ ) # Read what was copied
917
1003
  except Exception as e:
918
- console.print(f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]")
1004
+ console.print(
1005
+ f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]"
1006
+ )
919
1007
  except Exception as e:
920
- console.print(f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]")
921
-
1008
+ console.print(
1009
+ f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]"
1010
+ )
922
1011
 
923
1012
  # 2. Apply Golf's OTel default exporter setting if OTEL_TRACES_EXPORTER is not already set
924
1013
  if settings.opentelemetry_enabled and settings.opentelemetry_default_exporter:
925
1014
  if "OTEL_TRACES_EXPORTER" not in env_vars_to_write:
926
- env_vars_to_write["OTEL_TRACES_EXPORTER"] = settings.opentelemetry_default_exporter
927
- console.print(f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]")
1015
+ env_vars_to_write["OTEL_TRACES_EXPORTER"] = (
1016
+ settings.opentelemetry_default_exporter
1017
+ )
1018
+ console.print(
1019
+ f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]"
1020
+ )
928
1021
 
929
1022
  # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
930
1023
  # (Ensures service name defaults to project name if not specified in user's .env)
931
1024
  if settings.opentelemetry_enabled and settings.name:
932
1025
  if "OTEL_SERVICE_NAME" not in env_vars_to_write:
933
1026
  env_vars_to_write["OTEL_SERVICE_NAME"] = settings.name
934
-
1027
+
935
1028
  # 4. (Re-)Write the .env file in the output directory if there's anything to write
936
1029
  if env_vars_to_write:
937
1030
  try:
@@ -941,31 +1034,41 @@ def build_project(
941
1034
  # and handle existing quotes within the value.
942
1035
  if isinstance(value, str):
943
1036
  # Replace backslashes first, then double quotes
944
- processed_value = value.replace('\\', '\\\\') # Escape backslashes
945
- processed_value = processed_value.replace('"', '\\"') # Escape double quotes
946
- if ' ' in value or '#' in value or '\n' in value or '"' in value or "'" in value:
1037
+ processed_value = value.replace(
1038
+ "\\", "\\\\"
1039
+ ) # Escape backslashes
1040
+ processed_value = processed_value.replace(
1041
+ '"', '\\"'
1042
+ ) # Escape double quotes
1043
+ if (
1044
+ " " in value
1045
+ or "#" in value
1046
+ or "\n" in value
1047
+ or '"' in value
1048
+ or "'" in value
1049
+ ):
947
1050
  f.write(f'{key}="{processed_value}"\n')
948
1051
  else:
949
1052
  f.write(f"{key}={processed_value}\n")
950
- else: # For non-string values, write directly
1053
+ else: # For non-string values, write directly
951
1054
  f.write(f"{key}={value}\n")
952
1055
  except Exception as e:
953
- console.print(f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]")
1056
+ console.print(
1057
+ f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]"
1058
+ )
954
1059
  # --- END Enhanced .env handling ---
955
1060
 
956
1061
  # Show what we're building, with environment info
957
- console.print(f"[bold]Building [green]{settings.name}[/green] ({build_env} environment)[/bold]")
958
-
1062
+ console.print(
1063
+ f"[bold]Building [green]{settings.name}[/green] ({build_env} environment)[/bold]"
1064
+ )
1065
+
959
1066
  # Generate the code
960
1067
  generator = CodeGenerator(
961
- project_path,
962
- settings,
963
- output_dir,
964
- build_env=build_env,
965
- copy_env=copy_env
1068
+ project_path, settings, output_dir, build_env=build_env, copy_env=copy_env
966
1069
  )
967
1070
  generator.generate()
968
-
1071
+
969
1072
  # Create a simple README
970
1073
  readme_content = f"""# {settings.name}
971
1074
 
@@ -980,10 +1083,10 @@ python server.py
980
1083
 
981
1084
  This is a standalone FastMCP server generated by GolfMCP.
982
1085
  """
983
-
1086
+
984
1087
  with open(output_dir / "README.md", "w") as f:
985
1088
  f.write(readme_content)
986
-
1089
+
987
1090
  # Copy pyproject.toml with required dependencies
988
1091
  base_dependencies = [
989
1092
  "fastmcp>=2.0.0",
@@ -997,12 +1100,16 @@ This is a standalone FastMCP server generated by GolfMCP.
997
1100
  base_dependencies.extend(get_otel_dependencies())
998
1101
 
999
1102
  # Add authentication dependencies if enabled, before generating pyproject_content
1000
- provider_config, required_scopes = get_auth_config() # Ensure this is called to check for auth
1103
+ provider_config, required_scopes = (
1104
+ get_auth_config()
1105
+ ) # Ensure this is called to check for auth
1001
1106
  if provider_config:
1002
- base_dependencies.extend([
1003
- "pyjwt>=2.0.0",
1004
- "httpx>=0.20.0",
1005
- ])
1107
+ base_dependencies.extend(
1108
+ [
1109
+ "pyjwt>=2.0.0",
1110
+ "httpx>=0.20.0",
1111
+ ]
1112
+ )
1006
1113
 
1007
1114
  # Create the dependencies string
1008
1115
  dependencies_str = ",\n ".join([f'"{dep}"' for dep in base_dependencies])
@@ -1020,121 +1127,145 @@ dependencies = [
1020
1127
  {dependencies_str}
1021
1128
  ]
1022
1129
  """
1023
-
1130
+
1024
1131
  with open(output_dir / "pyproject.toml", "w") as f:
1025
1132
  f.write(pyproject_content)
1026
1133
 
1027
-
1028
1134
  # Always copy the auth module so it's available
1029
1135
  auth_dir = output_dir / "golf" / "auth"
1030
1136
  auth_dir.mkdir(parents=True, exist_ok=True)
1031
-
1137
+
1032
1138
  # Create __init__.py with needed exports
1033
1139
  with open(auth_dir / "__init__.py", "w") as f:
1034
- f.write("""\"\"\"Auth module for GolfMCP.\"\"\"
1140
+ f.write(
1141
+ """\"\"\"Auth module for GolfMCP.\"\"\"
1035
1142
 
1036
1143
  from golf.auth.provider import ProviderConfig
1037
1144
  from golf.auth.oauth import GolfOAuthProvider, create_callback_handler
1038
1145
  from golf.auth.helpers import get_access_token, get_provider_token, extract_token_from_header, get_api_key, set_api_key
1039
1146
  from golf.auth.api_key import configure_api_key, get_api_key_config
1040
- """)
1041
-
1147
+ """
1148
+ )
1149
+
1042
1150
  # Copy provider, oauth, and helper modules
1043
1151
  for module in ["provider.py", "oauth.py", "helpers.py", "api_key.py"]:
1044
1152
  src_file = Path(__file__).parent.parent.parent / "golf" / "auth" / module
1045
1153
  dst_file = auth_dir / module
1046
-
1154
+
1047
1155
  if src_file.exists():
1048
1156
  shutil.copy(src_file, dst_file)
1049
1157
  else:
1050
- console.print(f"[yellow]Warning: Could not find {src_file} to copy[/yellow]")
1051
-
1158
+ console.print(
1159
+ f"[yellow]Warning: Could not find {src_file} to copy[/yellow]"
1160
+ )
1161
+
1052
1162
  # Copy telemetry module if OpenTelemetry is enabled
1053
1163
  if settings.opentelemetry_enabled:
1054
1164
  telemetry_dir = output_dir / "golf" / "telemetry"
1055
1165
  telemetry_dir.mkdir(parents=True, exist_ok=True)
1056
-
1166
+
1057
1167
  # Copy telemetry __init__.py
1058
- src_init = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1168
+ src_init = (
1169
+ Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1170
+ )
1059
1171
  dst_init = telemetry_dir / "__init__.py"
1060
1172
  if src_init.exists():
1061
1173
  shutil.copy(src_init, dst_init)
1062
-
1174
+
1063
1175
  # Copy instrumentation module
1064
- src_instrumentation = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "instrumentation.py"
1176
+ src_instrumentation = (
1177
+ Path(__file__).parent.parent.parent
1178
+ / "golf"
1179
+ / "telemetry"
1180
+ / "instrumentation.py"
1181
+ )
1065
1182
  dst_instrumentation = telemetry_dir / "instrumentation.py"
1066
1183
  if src_instrumentation.exists():
1067
1184
  shutil.copy(src_instrumentation, dst_instrumentation)
1068
1185
  else:
1069
- console.print("[yellow]Warning: Could not find telemetry instrumentation module[/yellow]")
1070
-
1186
+ console.print(
1187
+ "[yellow]Warning: Could not find telemetry instrumentation module[/yellow]"
1188
+ )
1189
+
1071
1190
  # Check if auth routes need to be added
1072
1191
  provider_config, _ = get_auth_config()
1073
1192
  if provider_config:
1074
1193
  auth_routes_code = generate_auth_routes()
1075
-
1194
+
1076
1195
  server_file = output_dir / "server.py"
1077
1196
  if server_file.exists():
1078
- with open(server_file, "r") as f:
1197
+ with open(server_file) as f:
1079
1198
  server_code_content = f.read()
1080
-
1199
+
1081
1200
  # Add auth routes before the main block
1082
1201
  app_marker = 'if __name__ == "__main__":'
1083
1202
  app_pos = server_code_content.find(app_marker)
1084
1203
  if app_pos != -1:
1085
1204
  modified_code = (
1086
- server_code_content[:app_pos] +
1087
- auth_routes_code + "\n\n" +
1088
- server_code_content[app_pos:]
1205
+ server_code_content[:app_pos]
1206
+ + auth_routes_code
1207
+ + "\n\n"
1208
+ + server_code_content[app_pos:]
1089
1209
  )
1090
-
1210
+
1091
1211
  # Format with black before writing
1092
1212
  try:
1093
- final_code_to_write = black.format_str(modified_code, mode=black.Mode())
1213
+ final_code_to_write = black.format_str(
1214
+ modified_code, mode=black.Mode()
1215
+ )
1094
1216
  except Exception as e:
1095
- console.print(f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]")
1217
+ console.print(
1218
+ f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]"
1219
+ )
1096
1220
  final_code_to_write = modified_code
1097
-
1221
+
1098
1222
  with open(server_file, "w") as f:
1099
1223
  f.write(final_code_to_write)
1100
1224
  else:
1101
- console.print(f"[yellow]Warning: Could not find main block marker '{app_marker}' in {server_file} to inject auth routes.[/yellow]")
1225
+ console.print(
1226
+ f"[yellow]Warning: Could not find main block marker '{app_marker}' in {server_file} to inject auth routes.[/yellow]"
1227
+ )
1102
1228
 
1103
1229
 
1104
1230
  # Renamed function - was find_shared_modules
1105
- def find_common_files(project_path: Path, components: Dict[ComponentType, List[ParsedComponent]]) -> Dict[str, Path]:
1231
+ def find_common_files(
1232
+ project_path: Path, components: dict[ComponentType, list[ParsedComponent]]
1233
+ ) -> dict[str, Path]:
1106
1234
  """Find all common.py files used by components."""
1107
1235
  # We'll use the parser's functionality to find common files directly
1108
1236
  from golf.core.parser import parse_common_files
1237
+
1109
1238
  common_files = parse_common_files(project_path)
1110
-
1239
+
1111
1240
  # Return the found files without debug messages
1112
1241
  return common_files
1113
1242
 
1114
1243
 
1115
1244
  # Updated parameter name from shared_modules to common_files
1116
- def build_import_map(project_path: Path, common_files: Dict[str, Path]) -> Dict[str, str]:
1245
+ def build_import_map(
1246
+ project_path: Path, common_files: dict[str, Path]
1247
+ ) -> dict[str, str]:
1117
1248
  """Build a mapping of import paths to their new locations in the build output.
1118
-
1249
+
1119
1250
  This maps from original relative import paths to absolute import paths
1120
1251
  in the components directory structure.
1121
1252
  """
1122
1253
  import_map = {}
1123
-
1124
- for dir_path_str, file_path in common_files.items():
1254
+
1255
+ for dir_path_str, _file_path in common_files.items():
1125
1256
  # Convert string path to Path object
1126
1257
  dir_path = Path(dir_path_str)
1127
-
1258
+
1128
1259
  # Get the component type (tools, resources, prompts)
1129
1260
  component_type = None
1130
1261
  for part in dir_path.parts:
1131
1262
  if part in ["tools", "resources", "prompts"]:
1132
1263
  component_type = part
1133
1264
  break
1134
-
1265
+
1135
1266
  if not component_type:
1136
1267
  continue
1137
-
1268
+
1138
1269
  # Calculate the relative path within the component type
1139
1270
  try:
1140
1271
  rel_to_component = dir_path.relative_to(component_type)
@@ -1146,15 +1277,15 @@ def build_import_map(project_path: Path, common_files: Dict[str, Path]) -> Dict[
1146
1277
  # Replace path separators with dots
1147
1278
  path_parts = str(rel_to_component).replace("\\", "/").split("/")
1148
1279
  new_path = f"components.{component_type}.{'.'.join(path_parts)}"
1149
-
1280
+
1150
1281
  # Map both the directory and the common file
1151
1282
  orig_module = dir_path_str
1152
1283
  import_map[orig_module] = new_path
1153
-
1284
+
1154
1285
  # Also map the specific common module
1155
1286
  common_module = f"{dir_path_str}/common"
1156
1287
  import_map[common_module] = f"{new_path}.common"
1157
1288
  except ValueError:
1158
1289
  continue
1159
-
1160
- return import_map
1290
+
1291
+ return import_map