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