@@ -36,6 +36,7 @@ class ParsedComponent:
3636 parameters : list [str ] | None = None # For resources with URI params
3737 parent_module : str | None = None # For nested components
3838 entry_function : str | None = None # Store the name of the function to use
39+ annotations : dict [str , Any ] | None = None # Tool annotations for MCP hints
3940
4041
4142class AstParser :
@@ -223,17 +224,25 @@ def _process_entry_function(
223224 component .parameters = parameters
224225
225226 def _process_tool (self , component : ParsedComponent , tree : ast .Module ) -> None :
226- """Process a tool component to extract input/output schemas."""
227+ """Process a tool component to extract input/output schemas and annotations ."""
227228 # Look for Input and Output classes in the AST
228229 input_class = None
229230 output_class = None
231+ annotations = None
230232
231233 for node in tree .body :
232234 if isinstance (node , ast .ClassDef ):
233235 if node .name == "Input" :
234236 input_class = node
235237 elif node .name == "Output" :
236238 output_class = node
239+ # Look for annotations assignment
240+ elif isinstance (node , ast .Assign ):
241+ for target in node .targets :
242+ if isinstance (target , ast .Name ) and target .id == "annotations" :
243+ if isinstance (node .value , ast .Dict ):
244+ annotations = self ._extract_dict_from_ast (node .value )
245+ break
237246
238247 # Process Input class if found
239248 if input_class :
@@ -255,6 +264,10 @@ def _process_tool(self, component: ParsedComponent, tree: ast.Module) -> None:
255264 )
256265 break
257266
267+ # Store annotations if found
268+ if annotations :
269+ component .annotations = annotations
270+
258271 def _process_resource (self , component : ParsedComponent , tree : ast .Module ) -> None :
259272 """Process a resource component to extract URI template."""
260273 # Look for resource_uri assignment in the AST
@@ -437,6 +450,46 @@ def _type_hint_to_json_type(self, type_hint: str) -> str:
437450 # Default to string for unknown types
438451 return "string"
439452
453+ def _extract_dict_from_ast (self , dict_node : ast .Dict ) -> dict [str , Any ]:
454+ """Extract a dictionary from an AST Dict node.
455+
456+ This handles simple literal dictionaries with string keys and
457+ boolean/string/number values.
458+ """
459+ result = {}
460+
461+ for key , value in zip (dict_node .keys , dict_node .values , strict = False ):
462+ # Extract the key
463+ if isinstance (key , ast .Constant ) and isinstance (key .value , str ):
464+ key_str = key .value
465+ elif isinstance (key , ast .Str ): # For older Python versions
466+ key_str = key .s
467+ else :
468+ # Skip non-string keys
469+ continue
470+
471+ # Extract the value
472+ if isinstance (value , ast .Constant ):
473+ # Handles strings, numbers, booleans, None
474+ result [key_str ] = value .value
475+ elif isinstance (value , ast .Str ): # For older Python versions
476+ result [key_str ] = value .s
477+ elif isinstance (value , ast .Num ): # For older Python versions
478+ result [key_str ] = value .n
479+ elif isinstance (
480+ value , ast .NameConstant
481+ ): # For older Python versions (True/False/None)
482+ result [key_str ] = value .value
483+ elif isinstance (value , ast .Name ):
484+ # Handle True/False/None as names
485+ if value .id in ("True" , "False" , "None" ):
486+ result [key_str ] = {"True" : True , "False" : False , "None" : None }[
487+ value .id
488+ ]
489+ # We could add more complex value handling here if needed
490+
491+ return result
492+
440493
441494def parse_project (project_path : Path ) -> dict [ComponentType , list [ParsedComponent ]]:
442495 """Parse a GolfMCP project to extract all components."""
0 commit comments