@@ -168,8 +168,12 @@ def parse_file(self, file_path: Path) -> list[ParsedComponent]:
168168 elif component_type == ComponentType .PROMPT :
169169 self ._process_prompt (component , tree )
170170
171- # Set component name based on file path
172- component .name = self ._derive_component_name (file_path , component_type )
171+ # Set component name - use explicit decorator name if available, otherwise derive from path
172+ explicit_name = self ._extract_explicit_name_from_decorator (entry_function , component_type )
173+ if explicit_name :
174+ component .name = explicit_name
175+ else :
176+ component .name = self ._derive_component_name (file_path , component_type )
173177
174178 # Set parent module if it's in a nested structure
175179 if len (rel_path .parts ) > 2 : # More than just "tools/file.py"
@@ -214,6 +218,79 @@ def _extract_component_description(
214218
215219 return description
216220
221+ def _extract_explicit_name_from_decorator (
222+ self ,
223+ func_node : ast .FunctionDef | ast .AsyncFunctionDef ,
224+ component_type : ComponentType ,
225+ ) -> str | None :
226+ """Extract explicit name from @tool/@resource/@prompt decorator.
227+
228+ Handles both import patterns:
229+ - @tool(name="x") or @tool("x")
230+ - @golf.tool(name="x") or @golf.tool("x")
231+
232+ Args:
233+ func_node: The function AST node to check
234+ component_type: The type of component being parsed
235+
236+ Returns:
237+ The explicit name if found and valid, None otherwise.
238+ """
239+ # Map component type to expected decorator name
240+ decorator_names = {
241+ ComponentType .TOOL : "tool" ,
242+ ComponentType .RESOURCE : "resource" ,
243+ ComponentType .PROMPT : "prompt" ,
244+ }
245+ expected_decorator = decorator_names .get (component_type )
246+ if not expected_decorator :
247+ return None
248+
249+ for decorator in func_node .decorator_list :
250+ # Handle @tool(...) or @golf.tool(...)
251+ if isinstance (decorator , ast .Call ):
252+ func = decorator .func
253+
254+ # Check for @tool(...) pattern
255+ is_direct_decorator = isinstance (func , ast .Name ) and func .id == expected_decorator
256+
257+ # Check for @golf.tool(...) pattern
258+ is_qualified_decorator = (
259+ isinstance (func , ast .Attribute )
260+ and func .attr == expected_decorator
261+ and isinstance (func .value , ast .Name )
262+ and func .value .id == "golf"
263+ )
264+
265+ if is_direct_decorator or is_qualified_decorator :
266+ # Check for positional arg: @tool("name")
267+ if decorator .args :
268+ first_arg = decorator .args [0 ]
269+ if isinstance (first_arg , ast .Constant ) and isinstance (first_arg .value , str ):
270+ return first_arg .value
271+ else :
272+ # Non-string or dynamic value
273+ console .print (
274+ "[yellow]Warning: Decorator name must be a string literal, "
275+ "falling back to path-derived name[/yellow]"
276+ )
277+ return None
278+
279+ # Check for keyword arg: @tool(name="name")
280+ for keyword in decorator .keywords :
281+ if keyword .arg == "name" :
282+ if isinstance (keyword .value , ast .Constant ) and isinstance (keyword .value .value , str ):
283+ return keyword .value .value
284+ else :
285+ # Non-string or dynamic value
286+ console .print (
287+ "[yellow]Warning: Decorator name must be a string literal, "
288+ "falling back to path-derived name[/yellow]"
289+ )
290+ return None
291+
292+ return None
293+
217294 def _process_entry_function (
218295 self ,
219296 component : ParsedComponent ,
0 commit comments