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.
- golf/__init__.py +1 -1
- golf/auth/__init__.py +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +209 -87
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +81 -53
- golf/commands/run.py +30 -15
- golf/core/__init__.py +1 -1
- golf/core/builder.py +493 -362
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +216 -95
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +455 -310
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
- golf_mcp-0.1.13.dist-info/RECORD +55 -0
- golf_mcp-0.1.11.dist-info/RECORD +0 -55
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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:
|
|
45
|
-
self.manifest:
|
|
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) ->
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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) ->
|
|
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:
|
|
189
|
-
) ->
|
|
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
|
-
|
|
202
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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__(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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"
|
|
513
|
+
config["endpoint_path"] = "/sse" # Default SSE path for FastMCP
|
|
502
514
|
elif transport_type == "stdio":
|
|
503
|
-
config["endpoint_path"] = ""
|
|
515
|
+
config["endpoint_path"] = "" # No HTTP endpoint
|
|
504
516
|
else:
|
|
505
517
|
# Default to streamable-http
|
|
506
|
-
config["endpoint_path"] = "/mcp"
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
664
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 +=
|
|
686
|
-
|
|
726
|
+
registration += (
|
|
727
|
+
f"\nmcp.add_prompt({full_module_path}.export"
|
|
728
|
+
)
|
|
729
|
+
|
|
687
730
|
# Add the name parameter
|
|
688
|
-
registration += f
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
f
|
|
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(
|
|
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(
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
772
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
|
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(
|
|
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(
|
|
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
|
-
|
|
916
|
-
|
|
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(
|
|
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(
|
|
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"] =
|
|
927
|
-
|
|
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(
|
|
945
|
-
|
|
946
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
1004
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
1088
|
-
|
|
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(
|
|
1213
|
+
final_code_to_write = black.format_str(
|
|
1214
|
+
modified_code, mode=black.Mode()
|
|
1215
|
+
)
|
|
1094
1216
|
except Exception as e:
|
|
1095
|
-
console.print(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|