@@ -635,6 +635,178 @@ def get_scores() -> dict[str, int]:
635635        assert  result  ==  expected 
636636
637637
638+ class  TestToolMetadata :
639+     """Test tool metadata functionality.""" 
640+ 
641+     def  test_add_tool_with_metadata (self ):
642+         """Test adding a tool with metadata via ToolManager.""" 
643+ 
644+         def  process_data (input_data : str ) ->  str :
645+             """Process some data.""" 
646+             return  f"Processed: { input_data }  
647+ 
648+         metadata  =  {"ui" : {"type" : "form" , "fields" : ["input" ]}, "version" : "1.0" }
649+ 
650+         manager  =  ToolManager ()
651+         tool  =  manager .add_tool (process_data , meta = metadata )
652+ 
653+         assert  tool .meta  is  not None 
654+         assert  tool .meta  ==  metadata 
655+         assert  tool .meta ["ui" ]["type" ] ==  "form" 
656+         assert  tool .meta ["version" ] ==  "1.0" 
657+ 
658+     def  test_add_tool_without_metadata (self ):
659+         """Test that tools without metadata have None as meta value.""" 
660+ 
661+         def  simple_tool (x : int ) ->  int :
662+             """Simple tool.""" 
663+             return  x  *  2 
664+ 
665+         manager  =  ToolManager ()
666+         tool  =  manager .add_tool (simple_tool )
667+ 
668+         assert  tool .meta  is  None 
669+ 
670+     @pytest .mark .anyio  
671+     async  def  test_metadata_in_fastmcp_decorator (self ):
672+         """Test that metadata is correctly added via FastMCP.tool decorator.""" 
673+ 
674+         app  =  FastMCP ()
675+ 
676+         metadata  =  {"client" : {"ui_component" : "file_picker" }, "priority" : "high" }
677+ 
678+         @app .tool (meta = metadata ) 
679+         def  upload_file (filename : str ) ->  str :
680+             """Upload a file.""" 
681+             return  f"Uploaded: { filename }  
682+ 
683+         # Get the tool from the tool manager 
684+         tool  =  app ._tool_manager .get_tool ("upload_file" )
685+         assert  tool  is  not None 
686+         assert  tool .meta  is  not None 
687+         assert  tool .meta  ==  metadata 
688+         assert  tool .meta ["client" ]["ui_component" ] ==  "file_picker" 
689+         assert  tool .meta ["priority" ] ==  "high" 
690+ 
691+     @pytest .mark .anyio  
692+     async  def  test_metadata_in_list_tools (self ):
693+         """Test that metadata is included in MCPTool when listing tools.""" 
694+ 
695+         app  =  FastMCP ()
696+ 
697+         metadata  =  {
698+             "ui" : {"input_type" : "textarea" , "rows" : 5 },
699+             "tags" : ["text" , "processing" ],
700+         }
701+ 
702+         @app .tool (meta = metadata ) 
703+         def  analyze_text (text : str ) ->  dict [str , Any ]:
704+             """Analyze text content.""" 
705+             return  {"length" : len (text ), "words" : len (text .split ())}
706+ 
707+         tools  =  await  app .list_tools ()
708+         assert  len (tools ) ==  1 
709+         assert  tools [0 ].meta  is  not None 
710+         assert  tools [0 ].meta  ==  metadata 
711+ 
712+     @pytest .mark .anyio  
713+     async  def  test_multiple_tools_with_different_metadata (self ):
714+         """Test multiple tools with different metadata values.""" 
715+ 
716+         app  =  FastMCP ()
717+ 
718+         metadata1  =  {"ui" : "form" , "version" : 1 }
719+         metadata2  =  {"ui" : "picker" , "experimental" : True }
720+ 
721+         @app .tool (meta = metadata1 ) 
722+         def  tool1 (x : int ) ->  int :
723+             """First tool.""" 
724+             return  x 
725+ 
726+         @app .tool (meta = metadata2 ) 
727+         def  tool2 (y : str ) ->  str :
728+             """Second tool.""" 
729+             return  y 
730+ 
731+         @app .tool () 
732+         def  tool3 (z : bool ) ->  bool :
733+             """Third tool without metadata.""" 
734+             return  z 
735+ 
736+         tools  =  await  app .list_tools ()
737+         assert  len (tools ) ==  3 
738+ 
739+         # Find tools by name and check metadata 
740+         tools_by_name  =  {t .name : t  for  t  in  tools }
741+ 
742+         assert  tools_by_name ["tool1" ].meta  ==  metadata1 
743+         assert  tools_by_name ["tool2" ].meta  ==  metadata2 
744+         assert  tools_by_name ["tool3" ].meta  is  None 
745+ 
746+     def  test_metadata_with_complex_structure (self ):
747+         """Test metadata with complex nested structures.""" 
748+ 
749+         def  complex_tool (data : str ) ->  str :
750+             """Tool with complex metadata.""" 
751+             return  data 
752+ 
753+         metadata  =  {
754+             "ui" : {
755+                 "components" : [
756+                     {"type" : "input" , "name" : "field1" , "validation" : {"required" : True , "minLength" : 5 }},
757+                     {"type" : "select" , "name" : "field2" , "options" : ["a" , "b" , "c" ]},
758+                 ],
759+                 "layout" : {"columns" : 2 , "responsive" : True },
760+             },
761+             "permissions" : ["read" , "write" ],
762+             "tags" : ["data-processing" , "user-input" ],
763+             "version" : 2 ,
764+         }
765+ 
766+         manager  =  ToolManager ()
767+         tool  =  manager .add_tool (complex_tool , meta = metadata )
768+ 
769+         assert  tool .meta  is  not None 
770+         assert  tool .meta ["ui" ]["components" ][0 ]["validation" ]["minLength" ] ==  5 
771+         assert  tool .meta ["ui" ]["layout" ]["columns" ] ==  2 
772+         assert  "read"  in  tool .meta ["permissions" ]
773+         assert  "data-processing"  in  tool .meta ["tags" ]
774+ 
775+     def  test_metadata_empty_dict (self ):
776+         """Test that empty dict metadata is preserved.""" 
777+ 
778+         def  tool_with_empty_meta (x : int ) ->  int :
779+             """Tool with empty metadata.""" 
780+             return  x 
781+ 
782+         manager  =  ToolManager ()
783+         tool  =  manager .add_tool (tool_with_empty_meta , meta = {})
784+ 
785+         assert  tool .meta  is  not None 
786+         assert  tool .meta  ==  {}
787+ 
788+     @pytest .mark .anyio  
789+     async  def  test_metadata_with_annotations (self ):
790+         """Test that metadata and annotations can coexist.""" 
791+ 
792+         app  =  FastMCP ()
793+ 
794+         metadata  =  {"custom" : "value" }
795+         annotations  =  ToolAnnotations (title = "Combined Tool" , readOnlyHint = True )
796+ 
797+         @app .tool (meta = metadata , annotations = annotations ) 
798+         def  combined_tool (data : str ) ->  str :
799+             """Tool with both metadata and annotations.""" 
800+             return  data 
801+ 
802+         tools  =  await  app .list_tools ()
803+         assert  len (tools ) ==  1 
804+         assert  tools [0 ].meta  ==  metadata 
805+         assert  tools [0 ].annotations  is  not None 
806+         assert  tools [0 ].annotations .title  ==  "Combined Tool" 
807+         assert  tools [0 ].annotations .readOnlyHint  is  True 
808+ 
809+ 
638810class  TestRemoveTools :
639811    """Test tool removal functionality in the tool manager.""" 
640812
0 commit comments