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.
- 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 +202 -82
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +119 -80
- golf/commands/run.py +14 -13
- golf/core/__init__.py +1 -1
- golf/core/builder.py +478 -353
- 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 +169 -69
- 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 +781 -276
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
- golf_mcp-0.1.12.dist-info/RECORD +55 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
- golf_mcp-0.1.10.dist-info/RECORD +0 -55
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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#"
|
|
91
|
-
},
|
|
92
|
-
"annotations": {
|
|
93
|
-
"title": component.name.replace('-', ' ').title()
|
|
90
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
94
91
|
},
|
|
95
|
-
"
|
|
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"
|
|
551
|
-
|
|
552
|
-
|
|
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 =
|
|
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,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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
779
|
-
|
|
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
|
-
|
|
787
|
-
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
|
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(
|
|
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(
|
|
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
|
-
|
|
905
|
-
|
|
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(
|
|
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(
|
|
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"] =
|
|
916
|
-
|
|
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(
|
|
934
|
-
|
|
935
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
993
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
1077
|
-
|
|
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(
|
|
1196
|
+
final_code_to_write = black.format_str(
|
|
1197
|
+
modified_code, mode=black.Mode()
|
|
1198
|
+
)
|
|
1083
1199
|
except Exception as e:
|
|
1084
|
-
console.print(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|