@@ -521,7 +521,8 @@ func panics(f func()) (b bool) {
521521}
522522
523523func TestAddTool (t * testing.T ) {
524- // AddTool should panic if In or Out are not JSON objects.
524+ // AddTool should panic if In is not a JSON object.
525+ // Out may be of any type whose inferred JSON Schema is valid.
525526 s := NewServer (testImpl , nil )
526527 if ! panics (func () {
527528 AddTool (s , & Tool {Name : "T1" }, func (context.Context , * CallToolRequest , string ) (* CallToolResult , any , error ) { return nil , nil , nil })
@@ -535,12 +536,12 @@ func TestAddTool(t *testing.T) {
535536 }) {
536537 t .Error ("good In: expected no panic" )
537538 }
538- if ! panics (func () {
539- AddTool (s , & Tool {Name : "T2 " }, func (context.Context , * CallToolRequest , map [string ]any ) (* CallToolResult , int , error ) {
539+ if panics (func () {
540+ AddTool (s , & Tool {Name : "T3 " }, func (context.Context , * CallToolRequest , map [string ]any ) (* CallToolResult , int , error ) {
540541 return nil , 0 , nil
541542 })
542543 }) {
543- t .Error ("bad Out: expected panic" )
544+ t .Error ("primitive Out: expected no panic" )
544545 }
545546}
546547
@@ -653,6 +654,195 @@ func TestAddToolNilSchema(t *testing.T) {
653654 }
654655}
655656
657+ // TestAddToolNonObjectOutputSchema verifies the low-level
658+ // Server.AddTool accepts an output schema whose root type is not "object"
659+ // (array or primitive) and structured content of the matching shape
660+ // round-trips end-to-end.
661+ func TestAddToolNonObjectOutputSchema (t * testing.T ) {
662+ ctx := context .Background ()
663+
664+ for _ , tc := range []struct {
665+ name string
666+ outputSchema any
667+ content any
668+ want any
669+ }{
670+ {
671+ name : "array of objects" ,
672+ outputSchema : & jsonschema.Schema {Type : "array" , Items : & jsonschema.Schema {Type : "object" }},
673+ content : []any {map [string ]any {"id" : "u1" }, map [string ]any {"id" : "u2" }},
674+ want : []any {map [string ]any {"id" : "u1" }, map [string ]any {"id" : "u2" }},
675+ },
676+ {
677+ name : "primitive number (map-based schema)" ,
678+ outputSchema : map [string ]any {"type" : "number" },
679+ content : 42.0 ,
680+ want : 42.0 ,
681+ },
682+ {
683+ name : "primitive string (RawMessage schema)" ,
684+ outputSchema : json .RawMessage (`{"type":"string"}` ),
685+ content : "hello" ,
686+ want : "hello" ,
687+ },
688+ } {
689+ t .Run (tc .name , func (t * testing.T ) {
690+ server := NewServer (testImpl , nil )
691+ handler := func (ctx context.Context , req * CallToolRequest ) (* CallToolResult , error ) {
692+ return & CallToolResult {StructuredContent : tc .content }, nil
693+ }
694+ tool := & Tool {
695+ Name : "t" ,
696+ InputSchema : & jsonschema.Schema {Type : "object" },
697+ OutputSchema : tc .outputSchema ,
698+ }
699+ // Must not panic.
700+ server .AddTool (tool , handler )
701+
702+ ct , st := NewInMemoryTransports ()
703+ if _ , err := server .Connect (ctx , st , nil ); err != nil {
704+ t .Fatal (err )
705+ }
706+ client := NewClient (testImpl , nil )
707+ cs , err := client .Connect (ctx , ct , nil )
708+ if err != nil {
709+ t .Fatal (err )
710+ }
711+ defer cs .Close ()
712+
713+ res , err := cs .CallTool (ctx , & CallToolParams {Name : "t" })
714+ if err != nil {
715+ t .Fatal (err )
716+ }
717+ if res .IsError {
718+ t .Fatalf ("unexpected tool error: %v" , res .Content )
719+ }
720+ if diff := cmp .Diff (tc .want , res .StructuredContent ); diff != "" {
721+ t .Errorf ("structured content mismatch (-want +got):\n %s" , diff )
722+ }
723+ })
724+ }
725+ }
726+
727+ // TestAddToolGenericNonObjectOutput verifies SEP-2106 for the generic
728+ // AddTool[In, Out] helper: Out may be a slice or primitive whose inferred
729+ // JSON Schema has a non-object root.
730+ func TestAddToolGenericNonObjectOutput (t * testing.T ) {
731+ ctx := context .Background ()
732+
733+ type item struct {
734+ ID string `json:"id"`
735+ }
736+
737+ t .Run ("slice output" , func (t * testing.T ) {
738+ server := NewServer (testImpl , nil )
739+ AddTool (server , & Tool {Name : "list" },
740+ func (ctx context.Context , req * CallToolRequest , _ struct {}) (* CallToolResult , []item , error ) {
741+ return nil , []item {{ID : "u1" }, {ID : "u2" }}, nil
742+ })
743+
744+ ct , st := NewInMemoryTransports ()
745+ if _ , err := server .Connect (ctx , st , nil ); err != nil {
746+ t .Fatal (err )
747+ }
748+ client := NewClient (testImpl , nil )
749+ cs , err := client .Connect (ctx , ct , nil )
750+ if err != nil {
751+ t .Fatal (err )
752+ }
753+ defer cs .Close ()
754+
755+ // Verify tools/list reports an array-rooted output schema.
756+ // jsonschema-go infers ["null","array"] for slices since nil slices
757+ // are valid; accept either form.
758+ lt , err := cs .ListTools (ctx , nil )
759+ if err != nil {
760+ t .Fatal (err )
761+ }
762+ if len (lt .Tools ) != 1 {
763+ t .Fatalf ("got %d tools, want 1" , len (lt .Tools ))
764+ }
765+ gotSchema , _ := lt .Tools [0 ].OutputSchema .(map [string ]any )
766+ typeOK := false
767+ switch typ := gotSchema ["type" ].(type ) {
768+ case string :
769+ typeOK = typ == "array"
770+ case []any :
771+ for _ , e := range typ {
772+ if e == "array" {
773+ typeOK = true
774+ }
775+ }
776+ }
777+ if ! typeOK {
778+ t .Errorf ("output schema type = %v, want to include %q" , gotSchema ["type" ], "array" )
779+ }
780+
781+ res , err := cs .CallTool (ctx , & CallToolParams {Name : "list" })
782+ if err != nil {
783+ t .Fatal (err )
784+ }
785+ if res .IsError {
786+ t .Fatalf ("unexpected tool error: %v" , res .Content )
787+ }
788+ want := []any {map [string ]any {"id" : "u1" }, map [string ]any {"id" : "u2" }}
789+ if diff := cmp .Diff (want , res .StructuredContent ); diff != "" {
790+ t .Errorf ("structured content mismatch (-want +got):\n %s" , diff )
791+ }
792+ })
793+
794+ t .Run ("primitive output" , func (t * testing.T ) {
795+ server := NewServer (testImpl , nil )
796+ AddTool (server , & Tool {Name : "count" },
797+ func (ctx context.Context , req * CallToolRequest , _ struct {}) (* CallToolResult , int , error ) {
798+ return nil , 42 , nil
799+ })
800+
801+ ct , st := NewInMemoryTransports ()
802+ if _ , err := server .Connect (ctx , st , nil ); err != nil {
803+ t .Fatal (err )
804+ }
805+ client := NewClient (testImpl , nil )
806+ cs , err := client .Connect (ctx , ct , nil )
807+ if err != nil {
808+ t .Fatal (err )
809+ }
810+ defer cs .Close ()
811+
812+ res , err := cs .CallTool (ctx , & CallToolParams {Name : "count" })
813+ if err != nil {
814+ t .Fatal (err )
815+ }
816+ if res .IsError {
817+ t .Fatalf ("unexpected tool error: %v" , res .Content )
818+ }
819+ if diff := cmp .Diff (float64 (42 ), res .StructuredContent ); diff != "" {
820+ t .Errorf ("structured content mismatch (-want +got):\n %s" , diff )
821+ }
822+ })
823+ }
824+
825+ // TestAddToolInputSchemaComposition verifies SEP-2106 (input side): composition
826+ // keywords such as oneOf are allowed on the input schema alongside
827+ // type:"object".
828+ func TestAddToolInputSchemaComposition (t * testing.T ) {
829+ server := NewServer (testImpl , nil )
830+ tool := & Tool {
831+ Name : "lookup" ,
832+ InputSchema : & jsonschema.Schema {
833+ Type : "object" ,
834+ OneOf : []* jsonschema.Schema {
835+ {Required : []string {"id" }, Properties : map [string ]* jsonschema.Schema {"id" : {Type : "string" }}},
836+ {Required : []string {"name" }, Properties : map [string ]* jsonschema.Schema {"name" : {Type : "string" }}},
837+ },
838+ },
839+ }
840+ // Must not panic.
841+ server .AddTool (tool , func (ctx context.Context , req * CallToolRequest ) (* CallToolResult , error ) {
842+ return & CallToolResult {}, nil
843+ })
844+ }
845+
656846type schema = jsonschema.Schema
657847
658848func testToolForSchema [In , Out any ](t * testing.T , tool * Tool , in string , out Out , wantIn , wantOut any , wantErrContaining string ) {
0 commit comments