@@ -533,6 +533,273 @@ public async Task StructuredOutput_Disabled_ReturnsExpectedSchema<T>(T value)
533533 Assert . Null ( result . StructuredContent ) ;
534534 }
535535
536+ [ Fact ]
537+ public void OutputSchema_Options_OverridesReturnTypeSchema ( )
538+ {
539+ // When OutputSchema is set on options, it should be used instead of the return type's schema
540+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}""" ) . RootElement ;
541+ McpServerTool tool = McpServerTool . Create ( ( ) => "result" , new ( )
542+ {
543+ UseStructuredContent = true ,
544+ OutputSchema = outputSchema ,
545+ } ) ;
546+
547+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
548+ Assert . True ( tool . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var properties ) ) ;
549+ Assert . True ( properties . TryGetProperty ( "name" , out _ ) ) ;
550+ Assert . True ( properties . TryGetProperty ( "age" , out _ ) ) ;
551+ }
552+
553+ [ Fact ]
554+ public void OutputSchema_Options_WithCallToolResultReturn ( )
555+ {
556+ // When the tool returns CallToolResult, OutputSchema on options provides the advertised schema
557+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"object","properties":{"result":{"type":"string"}},"required":["result"]}""" ) . RootElement ;
558+ McpServerTool tool = McpServerTool . Create ( ( ) => new CallToolResult ( ) { Content = [ ] } , new ( )
559+ {
560+ UseStructuredContent = true ,
561+ OutputSchema = outputSchema ,
562+ } ) ;
563+
564+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
565+ Assert . Equal ( "object" , tool . ProtocolTool . OutputSchema . Value . GetProperty ( "type" ) . GetString ( ) ) ;
566+ Assert . True ( tool . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var properties ) ) ;
567+ Assert . True ( properties . TryGetProperty ( "result" , out _ ) ) ;
568+ }
569+
570+ [ Fact ]
571+ public async Task OutputSchema_Options_CallToolResult_PreservesStructuredContent ( )
572+ {
573+ // When tool returns CallToolResult with StructuredContent, it's preserved in the response
574+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"object","properties":{"value":{"type":"integer"}},"required":["value"]}""" ) . RootElement ;
575+ JsonElement structuredContent = JsonDocument . Parse ( """{"value":42}""" ) . RootElement ;
576+ McpServerTool tool = McpServerTool . Create ( ( ) => new CallToolResult ( )
577+ {
578+ Content = [ new TextContentBlock { Text = "42" } ] ,
579+ StructuredContent = structuredContent ,
580+ } , new ( )
581+ {
582+ Name = "tool" ,
583+ UseStructuredContent = true ,
584+ OutputSchema = outputSchema ,
585+ } ) ;
586+ var mockServer = new Mock < McpServer > ( ) ;
587+ var request = new RequestContext < CallToolRequestParams > ( mockServer . Object , CreateTestJsonRpcRequest ( ) )
588+ {
589+ Params = new CallToolRequestParams { Name = "tool" } ,
590+ } ;
591+
592+ var result = await tool . InvokeAsync ( request , TestContext . Current . CancellationToken ) ;
593+
594+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
595+ Assert . NotNull ( result . StructuredContent ) ;
596+ Assert . Equal ( 42 , result . StructuredContent . Value . GetProperty ( "value" ) . GetInt32 ( ) ) ;
597+ AssertMatchesJsonSchema ( tool . ProtocolTool . OutputSchema . Value , result . StructuredContent ) ;
598+ }
599+
600+ [ Fact ]
601+ public void OutputSchema_Options_RequiresUseStructuredContent ( )
602+ {
603+ // OutputSchema without UseStructuredContent=true should not produce an output schema
604+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"object","properties":{"name":{"type":"string"}}}""" ) . RootElement ;
605+ McpServerTool tool = McpServerTool . Create ( ( ) => "result" , new ( )
606+ {
607+ UseStructuredContent = false ,
608+ OutputSchema = outputSchema ,
609+ } ) ;
610+
611+ Assert . Null ( tool . ProtocolTool . OutputSchema ) ;
612+ }
613+
614+ [ Fact ]
615+ public void OutputSchema_Options_NonObjectSchema_GetsWrapped ( )
616+ {
617+ // Non-object output schema should be wrapped in a "result" property envelope
618+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"string"}""" ) . RootElement ;
619+ McpServerTool tool = McpServerTool . Create ( ( ) => "result" , new ( )
620+ {
621+ UseStructuredContent = true ,
622+ OutputSchema = outputSchema ,
623+ } ) ;
624+
625+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
626+ Assert . Equal ( "object" , tool . ProtocolTool . OutputSchema . Value . GetProperty ( "type" ) . GetString ( ) ) ;
627+ Assert . True ( tool . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var properties ) ) ;
628+ Assert . True ( properties . TryGetProperty ( "result" , out var resultProp ) ) ;
629+ Assert . Equal ( "string" , resultProp . GetProperty ( "type" ) . GetString ( ) ) ;
630+ }
631+
632+ [ Fact ]
633+ public void OutputSchema_Options_NullableObjectSchema_BecomesObject ( )
634+ {
635+ // ["object", "null"] type should be simplified to just "object"
636+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":["object","null"],"properties":{"name":{"type":"string"}}}""" ) . RootElement ;
637+ McpServerTool tool = McpServerTool . Create ( ( ) => "result" , new ( )
638+ {
639+ UseStructuredContent = true ,
640+ OutputSchema = outputSchema ,
641+ } ) ;
642+
643+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
644+ Assert . Equal ( "object" , tool . ProtocolTool . OutputSchema . Value . GetProperty ( "type" ) . GetString ( ) ) ;
645+ }
646+
647+ [ Fact ]
648+ public void OutputSchema_Attribute_WithType_GeneratesSchema ( )
649+ {
650+ McpServerTool tool = McpServerTool . Create ( ToolWithOutputSchemaAttribute , new ( ) { SerializerOptions = CreateSerializerOptionsWithPerson ( ) } ) ;
651+
652+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
653+ Assert . Equal ( "object" , tool . ProtocolTool . OutputSchema . Value . GetProperty ( "type" ) . GetString ( ) ) ;
654+ Assert . True ( tool . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var properties ) ) ;
655+ Assert . True ( properties . TryGetProperty ( "name" , out _ ) ) ;
656+ Assert . True ( properties . TryGetProperty ( "age" , out _ ) ) ;
657+ }
658+
659+ [ Fact ]
660+ public async Task OutputSchema_Attribute_CallToolResult_PreservesStructuredContent ( )
661+ {
662+ McpServerTool tool = McpServerTool . Create ( ToolWithOutputSchemaAttribute , new ( ) { SerializerOptions = CreateSerializerOptionsWithPerson ( ) } ) ;
663+
664+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
665+ Assert . Equal ( "object" , tool . ProtocolTool . OutputSchema . Value . GetProperty ( "type" ) . GetString ( ) ) ;
666+
667+ var mockServer = new Mock < McpServer > ( ) ;
668+ var request = new RequestContext < CallToolRequestParams > ( mockServer . Object , CreateTestJsonRpcRequest ( ) )
669+ {
670+ Params = new CallToolRequestParams { Name = "tool" } ,
671+ } ;
672+
673+ var result = await tool . InvokeAsync ( request , TestContext . Current . CancellationToken ) ;
674+
675+ Assert . NotNull ( result . StructuredContent ) ;
676+ Assert . Equal ( "John" , result . StructuredContent . Value . GetProperty ( "name" ) . GetString ( ) ) ;
677+ Assert . Equal ( 27 , result . StructuredContent . Value . GetProperty ( "age" ) . GetInt32 ( ) ) ;
678+ AssertMatchesJsonSchema ( tool . ProtocolTool . OutputSchema . Value , result . StructuredContent ) ;
679+ }
680+
681+ [ Fact ]
682+ public void OutputSchema_Attribute_WithoutUseStructuredContent_NoSchema ( )
683+ {
684+ // If UseStructuredContent is false but OutputSchema type is set, no output schema should be generated
685+ McpServerTool tool = McpServerTool . Create ( ToolWithOutputSchemaButNoStructuredContent , new ( ) { SerializerOptions = CreateSerializerOptionsWithPerson ( ) } ) ;
686+
687+ Assert . Null ( tool . ProtocolTool . OutputSchema ) ;
688+ }
689+
690+ [ Fact ]
691+ public void OutputSchema_Options_TakesPrecedenceOverAttribute ( )
692+ {
693+ // Options.OutputSchema should take precedence over attribute-derived schema
694+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"object","properties":{"custom":{"type":"boolean"}},"required":["custom"]}""" ) . RootElement ;
695+ McpServerTool tool = McpServerTool . Create ( ToolWithOutputSchemaAttribute , new ( )
696+ {
697+ OutputSchema = outputSchema ,
698+ } ) ;
699+
700+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
701+ Assert . True ( tool . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var properties ) ) ;
702+ Assert . True ( properties . TryGetProperty ( "custom" , out _ ) ) ;
703+ // Should not have Person's properties
704+ Assert . False ( properties . TryGetProperty ( "name" , out _ ) ) ;
705+ }
706+
707+ [ Fact ]
708+ public void OutputSchema_Options_Clone_PreservesValue ( )
709+ {
710+ // Verify that Clone() preserves the OutputSchema property
711+ JsonElement outputSchema = JsonDocument . Parse ( """{"type":"object","properties":{"x":{"type":"integer"}}}""" ) . RootElement ;
712+ McpServerTool tool1 = McpServerTool . Create ( ( ) => "result" , new ( )
713+ {
714+ Name = "tool1" ,
715+ UseStructuredContent = true ,
716+ OutputSchema = outputSchema ,
717+ } ) ;
718+
719+ // The output schema should be present since we set it
720+ Assert . NotNull ( tool1 . ProtocolTool . OutputSchema ) ;
721+ Assert . True ( tool1 . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var props ) ) ;
722+ Assert . True ( props . TryGetProperty ( "x" , out _ ) ) ;
723+ }
724+
725+ [ Fact ]
726+ public async Task OutputSchema_Options_PersonType_WithCallToolResult ( )
727+ {
728+ // Create output schema from Person type, tool returns CallToolResult with matching structured content
729+ JsonSerializerOptions serializerOptions = new ( ) { TypeInfoResolver = new DefaultJsonTypeInfoResolver ( ) } ;
730+ JsonElement outputSchema = AIJsonUtilities . CreateJsonSchema ( typeof ( Person ) , serializerOptions : serializerOptions ) ;
731+ Person person = new ( "Alice" , 30 ) ;
732+ JsonElement structuredContent = JsonSerializer . SerializeToElement ( person , serializerOptions ) ;
733+ McpServerTool tool = McpServerTool . Create ( ( ) => new CallToolResult ( )
734+ {
735+ Content = [ new TextContentBlock { Text = "Alice, 30" } ] ,
736+ StructuredContent = structuredContent ,
737+ } , new ( )
738+ {
739+ Name = "tool" ,
740+ UseStructuredContent = true ,
741+ OutputSchema = outputSchema ,
742+ SerializerOptions = serializerOptions ,
743+ } ) ;
744+ var mockServer = new Mock < McpServer > ( ) ;
745+ var request = new RequestContext < CallToolRequestParams > ( mockServer . Object , CreateTestJsonRpcRequest ( ) )
746+ {
747+ Params = new CallToolRequestParams { Name = "tool" } ,
748+ } ;
749+
750+ var result = await tool . InvokeAsync ( request , TestContext . Current . CancellationToken ) ;
751+
752+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
753+ Assert . NotNull ( result . StructuredContent ) ;
754+ AssertMatchesJsonSchema ( tool . ProtocolTool . OutputSchema . Value , result . StructuredContent ) ;
755+ }
756+
757+ [ Fact ]
758+ public async Task OutputSchema_Options_OverridesReturnTypeSchema_InvokeAndValidate ( )
759+ {
760+ // OutputSchema overrides return type schema; result should match the original return type, but schema is the override
761+ JsonSerializerOptions serializerOptions = new ( ) { TypeInfoResolver = new DefaultJsonTypeInfoResolver ( ) } ;
762+ JsonElement outputSchema = AIJsonUtilities . CreateJsonSchema ( typeof ( Person ) , serializerOptions : serializerOptions ) ;
763+ McpServerTool tool = McpServerTool . Create ( ( ) => new Person ( "Bob" , 25 ) , new ( )
764+ {
765+ Name = "tool" ,
766+ UseStructuredContent = true ,
767+ OutputSchema = outputSchema ,
768+ SerializerOptions = serializerOptions ,
769+ } ) ;
770+ var mockServer = new Mock < McpServer > ( ) ;
771+ var request = new RequestContext < CallToolRequestParams > ( mockServer . Object , CreateTestJsonRpcRequest ( ) )
772+ {
773+ Params = new CallToolRequestParams { Name = "tool" } ,
774+ } ;
775+
776+ var result = await tool . InvokeAsync ( request , TestContext . Current . CancellationToken ) ;
777+
778+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
779+ Assert . NotNull ( result . StructuredContent ) ;
780+ AssertMatchesJsonSchema ( tool . ProtocolTool . OutputSchema . Value , result . StructuredContent ) ;
781+ }
782+
783+ [ McpServerTool ( UseStructuredContent = true , OutputSchemaType = typeof ( Person ) ) ]
784+ private static CallToolResult ToolWithOutputSchemaAttribute ( )
785+ {
786+ var person = new Person ( "John" , 27 ) ;
787+ return new CallToolResult ( )
788+ {
789+ Content = [ new TextContentBlock { Text = $ "{ person . Name } , { person . Age } " } ] ,
790+ StructuredContent = JsonSerializer . SerializeToElement ( person , JsonContext2 . Default . Person ) ,
791+ } ;
792+ }
793+
794+ [ McpServerTool ( UseStructuredContent = false , OutputSchemaType = typeof ( Person ) ) ]
795+ private static CallToolResult ToolWithOutputSchemaButNoStructuredContent ( )
796+ {
797+ return new CallToolResult ( )
798+ {
799+ Content = [ new TextContentBlock { Text = "result" } ] ,
800+ } ;
801+ }
802+
536803 [ Theory ]
537804 [ InlineData ( JsonNumberHandling . Strict ) ]
538805 [ InlineData ( JsonNumberHandling . AllowReadingFromString ) ]
@@ -679,6 +946,13 @@ Instance JSON document does not match the specified schema.
679946
680947 record Person ( string Name , int Age ) ;
681948
949+ private static JsonSerializerOptions CreateSerializerOptionsWithPerson ( )
950+ {
951+ JsonSerializerOptions options = new ( McpJsonUtilities . DefaultOptions ) ;
952+ options . TypeInfoResolverChain . Add ( JsonContext2 . Default ) ;
953+ return options ;
954+ }
955+
682956 [ Fact ]
683957 public void SupportsIconsInCreateOptions ( )
684958 {
@@ -928,5 +1202,6 @@ private static string SyncTool()
9281202 [ JsonSerializable ( typeof ( List < string > ) ) ]
9291203 [ JsonSerializable ( typeof ( int ? ) ) ]
9301204 [ JsonSerializable ( typeof ( DateTimeOffset ? ) ) ]
1205+ [ JsonSerializable ( typeof ( Person ) ) ]
9311206 partial class JsonContext2 : JsonSerializerContext ;
9321207}
0 commit comments