golf-mcp 0.1.10__py3-none-any.whl → 0.1.12__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 +202 -82
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +119 -80
  12. golf/commands/run.py +14 -13
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +478 -353
  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 +169 -69
  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 +781 -276
  42. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.12.dist-info/RECORD +55 -0
  44. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
  45. golf_mcp-0.1.10.dist-info/RECORD +0 -55
  46. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.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#"
91
- },
92
- "annotations": {
93
- "title": component.name.replace('-', ' ').title()
90
+ "$schema": "http://json-schema.org/draft-07/schema#",
94
91
  },
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":
551
- imports.append("import uvicorn")
552
- elif self.settings.transport in ["streamable-http", "http"]:
562
+ if self.settings.transport == "sse" or self.settings.transport in [
563
+ "streamable-http",
564
+ "http",
565
+ ]:
553
566
  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,109 +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
- " # Run with the configured app",
769
- " uvicorn.run(app, host=host, port=port, log_level=\"info\")"
770
- ])
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
+ )
771
818
  else:
772
- main_code.extend([
773
- " # For SSE, FastMCP's run method handles auth integration better",
774
- " mcp.run(transport=\"sse\", host=host, port=port, log_level=\"info\")"
775
- ])
819
+ main_code.extend(
820
+ [
821
+ " # For SSE, get the app to add middleware",
822
+ ' app = mcp.http_app(transport="sse")',
823
+ ]
824
+ )
825
+
826
+ # Add OpenTelemetry middleware to the SSE app if enabled
827
+ if self.settings.opentelemetry_enabled:
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
+ )
776
842
  elif self.settings.transport in ["streamable-http", "http"]:
777
- main_code.extend([
778
- " # Create HTTP app and run with uvicorn",
779
- " app = mcp.http_app()",
780
- ])
781
-
843
+ main_code.extend(
844
+ [
845
+ " # Create HTTP app and run with uvicorn",
846
+ " app = mcp.http_app()",
847
+ ]
848
+ )
849
+
782
850
  # Check if we need to add API key middleware
783
851
  api_key_config = get_api_key_config()
784
852
  if auth_components.get("has_auth") and api_key_config:
785
- main_code.extend([
786
- " # Add API key middleware",
787
- " app.add_middleware(ApiKeyMiddleware)",
788
- ])
789
-
853
+ main_code.extend(
854
+ [
855
+ " # Add API key middleware",
856
+ " app.add_middleware(ApiKeyMiddleware)",
857
+ ]
858
+ )
859
+
790
860
  # Add OpenTelemetry middleware to the HTTP app if enabled
791
861
  if self.settings.opentelemetry_enabled:
792
- main_code.extend([
793
- " # Apply OpenTelemetry middleware to the HTTP app",
794
- " from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware",
795
- " app = OpenTelemetryMiddleware(app)",
796
- ])
797
-
798
- main_code.extend([
799
- " uvicorn.run(app, host=host, port=port, log_level=\"info\")"
800
- ])
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
+ )
801
873
  else:
802
874
  # For stdio transport, use mcp.run()
803
- main_code.extend([
804
- " # Run with stdio transport",
805
- " mcp.run(transport=\"stdio\")"
806
- ])
807
-
875
+ main_code.extend(
876
+ [" # Run with stdio transport", ' mcp.run(transport="stdio")']
877
+ )
878
+
808
879
  # Combine all sections
809
- # Order: imports, env_section, auth_setup, server_code (mcp init),
880
+ # Order: imports, env_section, auth_setup, server_code (mcp init),
810
881
  # post_init (API key middleware), component_registrations, main_code (run block)
811
882
  code = "\n".join(
812
- imports +
813
- env_section +
814
- auth_setup_code +
815
- server_code_lines +
816
- component_registrations +
817
- main_code
883
+ imports
884
+ + env_section
885
+ + auth_setup_code
886
+ + server_code_lines
887
+ + component_registrations
888
+ + main_code
818
889
  )
819
-
890
+
820
891
  # Format with black
821
892
  try:
822
893
  code = black.format_str(code, mode=black.Mode())
823
894
  except Exception as e:
824
895
  console.print(f"[yellow]Warning: Could not format server.py: {e}[/yellow]")
825
-
896
+
826
897
  # Write to file
827
898
  with open(server_file, "w") as f:
828
899
  f.write(code)
829
900
 
830
901
 
831
902
  def build_project(
832
- project_path: Path,
833
- settings: Settings,
903
+ project_path: Path,
904
+ settings: Settings,
834
905
  output_dir: Path,
835
906
  build_env: str = "prod",
836
- copy_env: bool = False
907
+ copy_env: bool = False,
837
908
  ) -> None:
838
909
  """Build a standalone FastMCP application from a GolfMCP project.
839
-
910
+
840
911
  Args:
841
912
  project_path: Path to the project directory
842
913
  settings: Project settings
@@ -851,38 +922,41 @@ def build_project(
851
922
  # Save the current directory and path
852
923
  original_dir = os.getcwd()
853
924
  original_path = sys.path.copy()
854
-
925
+
855
926
  # Change to the project directory and add it to Python path
856
927
  os.chdir(project_path)
857
928
  sys.path.insert(0, str(project_path))
858
-
929
+
859
930
  # Execute the pre_build script
860
931
  with open(pre_build_path) as f:
861
932
  script_content = f.read()
862
-
933
+
863
934
  # Print the first few lines for debugging
864
- preview = "\n".join(script_content.split("\n")[:5]) + "\n..."
865
-
935
+ "\n".join(script_content.split("\n")[:5]) + "\n..."
936
+
866
937
  # Use exec to run the script as a module
867
- code = compile(script_content, str(pre_build_path), 'exec')
938
+ code = compile(script_content, str(pre_build_path), "exec")
868
939
  exec(code, {})
869
-
940
+
870
941
  # Check if auth was configured by the script
871
942
  provider, scopes = get_auth_config()
872
-
943
+
873
944
  # Restore original directory and path
874
945
  os.chdir(original_dir)
875
946
  sys.path = original_path
876
-
947
+
877
948
  except Exception as e:
878
949
  console.print(f"[red]Error executing pre_build.py: {str(e)}[/red]")
879
950
  import traceback
951
+
880
952
  console.print(f"[red]{traceback.format_exc()}[/red]")
881
-
953
+
882
954
  # Clear the output directory if it exists
883
955
  if output_dir.exists():
884
956
  shutil.rmtree(output_dir)
885
- output_dir.mkdir(parents=True, exist_ok=True) # Ensure output_dir exists after clearing
957
+ output_dir.mkdir(
958
+ parents=True, exist_ok=True
959
+ ) # Ensure output_dir exists after clearing
886
960
 
887
961
  # --- BEGIN Enhanced .env handling ---
888
962
  env_vars_to_write = {}
@@ -894,33 +968,46 @@ def build_project(
894
968
  if project_env_file.exists():
895
969
  try:
896
970
  from dotenv import dotenv_values
971
+
897
972
  env_vars_to_write.update(dotenv_values(project_env_file))
898
973
  except ImportError:
899
- console.print("[yellow]Warning: python-dotenv is not installed. Cannot read existing .env file for rich merging. Copying directly.[/yellow]")
974
+ console.print(
975
+ "[yellow]Warning: python-dotenv is not installed. Cannot read existing .env file for rich merging. Copying directly.[/yellow]"
976
+ )
900
977
  try:
901
978
  shutil.copy(project_env_file, env_file_path)
902
979
  # If direct copy happens, re-read for step 2 & 3 to respect its content
903
980
  if env_file_path.exists():
904
- from dotenv import dotenv_values
905
- env_vars_to_write.update(dotenv_values(env_file_path)) # Read what was copied
981
+ from dotenv import dotenv_values
982
+
983
+ env_vars_to_write.update(
984
+ dotenv_values(env_file_path)
985
+ ) # Read what was copied
906
986
  except Exception as e:
907
- console.print(f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]")
987
+ console.print(
988
+ f"[yellow]Warning: Could not copy project .env file: {e}[/yellow]"
989
+ )
908
990
  except Exception as e:
909
- console.print(f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]")
910
-
991
+ console.print(
992
+ f"[yellow]Warning: Error reading project .env file content: {e}[/yellow]"
993
+ )
911
994
 
912
995
  # 2. Apply Golf's OTel default exporter setting if OTEL_TRACES_EXPORTER is not already set
913
996
  if settings.opentelemetry_enabled and settings.opentelemetry_default_exporter:
914
997
  if "OTEL_TRACES_EXPORTER" not in env_vars_to_write:
915
- env_vars_to_write["OTEL_TRACES_EXPORTER"] = settings.opentelemetry_default_exporter
916
- console.print(f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]")
998
+ env_vars_to_write["OTEL_TRACES_EXPORTER"] = (
999
+ settings.opentelemetry_default_exporter
1000
+ )
1001
+ console.print(
1002
+ f"[info]Setting OTEL_TRACES_EXPORTER to '{settings.opentelemetry_default_exporter}' from golf.json in built app's .env[/info]"
1003
+ )
917
1004
 
918
1005
  # 3. Apply Golf's project name as OTEL_SERVICE_NAME if not already set
919
1006
  # (Ensures service name defaults to project name if not specified in user's .env)
920
1007
  if settings.opentelemetry_enabled and settings.name:
921
1008
  if "OTEL_SERVICE_NAME" not in env_vars_to_write:
922
1009
  env_vars_to_write["OTEL_SERVICE_NAME"] = settings.name
923
-
1010
+
924
1011
  # 4. (Re-)Write the .env file in the output directory if there's anything to write
925
1012
  if env_vars_to_write:
926
1013
  try:
@@ -930,31 +1017,41 @@ def build_project(
930
1017
  # and handle existing quotes within the value.
931
1018
  if isinstance(value, str):
932
1019
  # Replace backslashes first, then double quotes
933
- processed_value = value.replace('\\', '\\\\') # Escape backslashes
934
- processed_value = processed_value.replace('"', '\\"') # Escape double quotes
935
- if ' ' in value or '#' in value or '\n' in value or '"' in value or "'" in value:
1020
+ processed_value = value.replace(
1021
+ "\\", "\\\\"
1022
+ ) # Escape backslashes
1023
+ processed_value = processed_value.replace(
1024
+ '"', '\\"'
1025
+ ) # Escape double quotes
1026
+ if (
1027
+ " " in value
1028
+ or "#" in value
1029
+ or "\n" in value
1030
+ or '"' in value
1031
+ or "'" in value
1032
+ ):
936
1033
  f.write(f'{key}="{processed_value}"\n')
937
1034
  else:
938
1035
  f.write(f"{key}={processed_value}\n")
939
- else: # For non-string values, write directly
1036
+ else: # For non-string values, write directly
940
1037
  f.write(f"{key}={value}\n")
941
1038
  except Exception as e:
942
- console.print(f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]")
1039
+ console.print(
1040
+ f"[yellow]Warning: Could not write .env file to output directory: {e}[/yellow]"
1041
+ )
943
1042
  # --- END Enhanced .env handling ---
944
1043
 
945
1044
  # Show what we're building, with environment info
946
- console.print(f"[bold]Building [green]{settings.name}[/green] ({build_env} environment)[/bold]")
947
-
1045
+ console.print(
1046
+ f"[bold]Building [green]{settings.name}[/green] ({build_env} environment)[/bold]"
1047
+ )
1048
+
948
1049
  # Generate the code
949
1050
  generator = CodeGenerator(
950
- project_path,
951
- settings,
952
- output_dir,
953
- build_env=build_env,
954
- copy_env=copy_env
1051
+ project_path, settings, output_dir, build_env=build_env, copy_env=copy_env
955
1052
  )
956
1053
  generator.generate()
957
-
1054
+
958
1055
  # Create a simple README
959
1056
  readme_content = f"""# {settings.name}
960
1057
 
@@ -969,10 +1066,10 @@ python server.py
969
1066
 
970
1067
  This is a standalone FastMCP server generated by GolfMCP.
971
1068
  """
972
-
1069
+
973
1070
  with open(output_dir / "README.md", "w") as f:
974
1071
  f.write(readme_content)
975
-
1072
+
976
1073
  # Copy pyproject.toml with required dependencies
977
1074
  base_dependencies = [
978
1075
  "fastmcp>=2.0.0",
@@ -986,12 +1083,16 @@ This is a standalone FastMCP server generated by GolfMCP.
986
1083
  base_dependencies.extend(get_otel_dependencies())
987
1084
 
988
1085
  # Add authentication dependencies if enabled, before generating pyproject_content
989
- provider_config, required_scopes = get_auth_config() # Ensure this is called to check for auth
1086
+ provider_config, required_scopes = (
1087
+ get_auth_config()
1088
+ ) # Ensure this is called to check for auth
990
1089
  if provider_config:
991
- base_dependencies.extend([
992
- "pyjwt>=2.0.0",
993
- "httpx>=0.20.0",
994
- ])
1090
+ base_dependencies.extend(
1091
+ [
1092
+ "pyjwt>=2.0.0",
1093
+ "httpx>=0.20.0",
1094
+ ]
1095
+ )
995
1096
 
996
1097
  # Create the dependencies string
997
1098
  dependencies_str = ",\n ".join([f'"{dep}"' for dep in base_dependencies])
@@ -1009,121 +1110,145 @@ dependencies = [
1009
1110
  {dependencies_str}
1010
1111
  ]
1011
1112
  """
1012
-
1113
+
1013
1114
  with open(output_dir / "pyproject.toml", "w") as f:
1014
1115
  f.write(pyproject_content)
1015
1116
 
1016
-
1017
1117
  # Always copy the auth module so it's available
1018
1118
  auth_dir = output_dir / "golf" / "auth"
1019
1119
  auth_dir.mkdir(parents=True, exist_ok=True)
1020
-
1120
+
1021
1121
  # Create __init__.py with needed exports
1022
1122
  with open(auth_dir / "__init__.py", "w") as f:
1023
- f.write("""\"\"\"Auth module for GolfMCP.\"\"\"
1123
+ f.write(
1124
+ """\"\"\"Auth module for GolfMCP.\"\"\"
1024
1125
 
1025
1126
  from golf.auth.provider import ProviderConfig
1026
1127
  from golf.auth.oauth import GolfOAuthProvider, create_callback_handler
1027
1128
  from golf.auth.helpers import get_access_token, get_provider_token, extract_token_from_header, get_api_key, set_api_key
1028
1129
  from golf.auth.api_key import configure_api_key, get_api_key_config
1029
- """)
1030
-
1130
+ """
1131
+ )
1132
+
1031
1133
  # Copy provider, oauth, and helper modules
1032
1134
  for module in ["provider.py", "oauth.py", "helpers.py", "api_key.py"]:
1033
1135
  src_file = Path(__file__).parent.parent.parent / "golf" / "auth" / module
1034
1136
  dst_file = auth_dir / module
1035
-
1137
+
1036
1138
  if src_file.exists():
1037
1139
  shutil.copy(src_file, dst_file)
1038
1140
  else:
1039
- console.print(f"[yellow]Warning: Could not find {src_file} to copy[/yellow]")
1040
-
1141
+ console.print(
1142
+ f"[yellow]Warning: Could not find {src_file} to copy[/yellow]"
1143
+ )
1144
+
1041
1145
  # Copy telemetry module if OpenTelemetry is enabled
1042
1146
  if settings.opentelemetry_enabled:
1043
1147
  telemetry_dir = output_dir / "golf" / "telemetry"
1044
1148
  telemetry_dir.mkdir(parents=True, exist_ok=True)
1045
-
1149
+
1046
1150
  # Copy telemetry __init__.py
1047
- src_init = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1151
+ src_init = (
1152
+ Path(__file__).parent.parent.parent / "golf" / "telemetry" / "__init__.py"
1153
+ )
1048
1154
  dst_init = telemetry_dir / "__init__.py"
1049
1155
  if src_init.exists():
1050
1156
  shutil.copy(src_init, dst_init)
1051
-
1157
+
1052
1158
  # Copy instrumentation module
1053
- src_instrumentation = Path(__file__).parent.parent.parent / "golf" / "telemetry" / "instrumentation.py"
1159
+ src_instrumentation = (
1160
+ Path(__file__).parent.parent.parent
1161
+ / "golf"
1162
+ / "telemetry"
1163
+ / "instrumentation.py"
1164
+ )
1054
1165
  dst_instrumentation = telemetry_dir / "instrumentation.py"
1055
1166
  if src_instrumentation.exists():
1056
1167
  shutil.copy(src_instrumentation, dst_instrumentation)
1057
1168
  else:
1058
- console.print("[yellow]Warning: Could not find telemetry instrumentation module[/yellow]")
1059
-
1169
+ console.print(
1170
+ "[yellow]Warning: Could not find telemetry instrumentation module[/yellow]"
1171
+ )
1172
+
1060
1173
  # Check if auth routes need to be added
1061
1174
  provider_config, _ = get_auth_config()
1062
1175
  if provider_config:
1063
1176
  auth_routes_code = generate_auth_routes()
1064
-
1177
+
1065
1178
  server_file = output_dir / "server.py"
1066
1179
  if server_file.exists():
1067
- with open(server_file, "r") as f:
1180
+ with open(server_file) as f:
1068
1181
  server_code_content = f.read()
1069
-
1182
+
1070
1183
  # Add auth routes before the main block
1071
1184
  app_marker = 'if __name__ == "__main__":'
1072
1185
  app_pos = server_code_content.find(app_marker)
1073
1186
  if app_pos != -1:
1074
1187
  modified_code = (
1075
- server_code_content[:app_pos] +
1076
- auth_routes_code + "\n\n" +
1077
- server_code_content[app_pos:]
1188
+ server_code_content[:app_pos]
1189
+ + auth_routes_code
1190
+ + "\n\n"
1191
+ + server_code_content[app_pos:]
1078
1192
  )
1079
-
1193
+
1080
1194
  # Format with black before writing
1081
1195
  try:
1082
- final_code_to_write = black.format_str(modified_code, mode=black.Mode())
1196
+ final_code_to_write = black.format_str(
1197
+ modified_code, mode=black.Mode()
1198
+ )
1083
1199
  except Exception as e:
1084
- console.print(f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]")
1200
+ console.print(
1201
+ f"[yellow]Warning: Could not format server.py after auth routes injection: {e}[/yellow]"
1202
+ )
1085
1203
  final_code_to_write = modified_code
1086
-
1204
+
1087
1205
  with open(server_file, "w") as f:
1088
1206
  f.write(final_code_to_write)
1089
1207
  else:
1090
- console.print(f"[yellow]Warning: Could not find main block marker '{app_marker}' in {server_file} to inject auth routes.[/yellow]")
1208
+ console.print(
1209
+ f"[yellow]Warning: Could not find main block marker '{app_marker}' in {server_file} to inject auth routes.[/yellow]"
1210
+ )
1091
1211
 
1092
1212
 
1093
1213
  # Renamed function - was find_shared_modules
1094
- def find_common_files(project_path: Path, components: Dict[ComponentType, List[ParsedComponent]]) -> Dict[str, Path]:
1214
+ def find_common_files(
1215
+ project_path: Path, components: dict[ComponentType, list[ParsedComponent]]
1216
+ ) -> dict[str, Path]:
1095
1217
  """Find all common.py files used by components."""
1096
1218
  # We'll use the parser's functionality to find common files directly
1097
1219
  from golf.core.parser import parse_common_files
1220
+
1098
1221
  common_files = parse_common_files(project_path)
1099
-
1222
+
1100
1223
  # Return the found files without debug messages
1101
1224
  return common_files
1102
1225
 
1103
1226
 
1104
1227
  # Updated parameter name from shared_modules to common_files
1105
- def build_import_map(project_path: Path, common_files: Dict[str, Path]) -> Dict[str, str]:
1228
+ def build_import_map(
1229
+ project_path: Path, common_files: dict[str, Path]
1230
+ ) -> dict[str, str]:
1106
1231
  """Build a mapping of import paths to their new locations in the build output.
1107
-
1232
+
1108
1233
  This maps from original relative import paths to absolute import paths
1109
1234
  in the components directory structure.
1110
1235
  """
1111
1236
  import_map = {}
1112
-
1113
- for dir_path_str, file_path in common_files.items():
1237
+
1238
+ for dir_path_str, _file_path in common_files.items():
1114
1239
  # Convert string path to Path object
1115
1240
  dir_path = Path(dir_path_str)
1116
-
1241
+
1117
1242
  # Get the component type (tools, resources, prompts)
1118
1243
  component_type = None
1119
1244
  for part in dir_path.parts:
1120
1245
  if part in ["tools", "resources", "prompts"]:
1121
1246
  component_type = part
1122
1247
  break
1123
-
1248
+
1124
1249
  if not component_type:
1125
1250
  continue
1126
-
1251
+
1127
1252
  # Calculate the relative path within the component type
1128
1253
  try:
1129
1254
  rel_to_component = dir_path.relative_to(component_type)
@@ -1135,15 +1260,15 @@ def build_import_map(project_path: Path, common_files: Dict[str, Path]) -> Dict[
1135
1260
  # Replace path separators with dots
1136
1261
  path_parts = str(rel_to_component).replace("\\", "/").split("/")
1137
1262
  new_path = f"components.{component_type}.{'.'.join(path_parts)}"
1138
-
1263
+
1139
1264
  # Map both the directory and the common file
1140
1265
  orig_module = dir_path_str
1141
1266
  import_map[orig_module] = new_path
1142
-
1267
+
1143
1268
  # Also map the specific common module
1144
1269
  common_module = f"{dir_path_str}/common"
1145
1270
  import_map[common_module] = f"{new_path}.common"
1146
1271
  except ValueError:
1147
1272
  continue
1148
-
1149
- return import_map
1273
+
1274
+ return import_map