@@ -533,6 +533,146 @@ public async Task StructuredOutput_Disabled_ReturnsExpectedSchema<T>(T value)
533533 Assert . Null ( result . StructuredContent ) ;
534534 }
535535
536+ [ Fact ]
537+ public void OutputSchema_ViaOptions_SetsSchemaDirectly ( )
538+ {
539+ var schemaDoc = JsonDocument . Parse ( """{"type":"object","properties":{"result":{"type":"string"}}}""" ) ;
540+ McpServerTool tool = McpServerTool . Create ( ( ) => new CallToolResult ( ) { Content = [ ] } , new ( )
541+ {
542+ OutputSchema = schemaDoc . RootElement ,
543+ } ) ;
544+
545+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
546+ Assert . True ( JsonElement . DeepEquals ( schemaDoc . RootElement , tool . ProtocolTool . OutputSchema . Value ) ) ;
547+ }
548+
549+ [ Fact ]
550+ public void OutputSchema_ViaOptions_ForcesStructuredContentEvenIfDisabled ( )
551+ {
552+ var schemaDoc = JsonDocument . Parse ( """{"type":"object","properties":{"n":{"type":"string"},"a":{"type":"integer"}},"required":["n","a"]}""" ) ;
553+ McpServerTool tool = McpServerTool . Create ( ( string input ) => new CallToolResult ( ) { Content = [ ] } , new ( )
554+ {
555+ UseStructuredContent = false ,
556+ OutputSchema = schemaDoc . RootElement ,
557+ } ) ;
558+
559+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
560+ Assert . True ( JsonElement . DeepEquals ( schemaDoc . RootElement , tool . ProtocolTool . OutputSchema . Value ) ) ;
561+ }
562+
563+ [ Fact ]
564+ public void OutputSchema_ViaOptions_TakesPrecedenceOverReturnTypeSchema ( )
565+ {
566+ var overrideDoc = JsonDocument . Parse ( """{"type":"object","properties":{"custom":{"type":"boolean"}}}""" ) ;
567+ JsonSerializerOptions serOpts = new ( ) { TypeInfoResolver = new DefaultJsonTypeInfoResolver ( ) } ;
568+ McpServerTool tool = McpServerTool . Create ( ( ) => new Person ( "Alice" , 30 ) , new ( )
569+ {
570+ UseStructuredContent = true ,
571+ OutputSchema = overrideDoc . RootElement ,
572+ SerializerOptions = serOpts ,
573+ } ) ;
574+
575+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
576+ Assert . True ( JsonElement . DeepEquals ( overrideDoc . RootElement , tool . ProtocolTool . OutputSchema . Value ) ) ;
577+ }
578+
579+ [ Fact ]
580+ public void OutputSchemaType_ViaAttribute_ProducesExpectedSchema ( )
581+ {
582+ JsonSerializerOptions serOpts = new ( ) { TypeInfoResolver = new DefaultJsonTypeInfoResolver ( ) } ;
583+ McpServerTool tool = McpServerTool . Create (
584+ typeof ( OutputSchemaTypeTools ) . GetMethod ( nameof ( OutputSchemaTypeTools . ToolReturningCallToolResultWithSchemaType ) ) ! ,
585+ target : null ,
586+ new ( ) { SerializerOptions = serOpts } ) ;
587+
588+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
589+ Assert . Equal ( "object" , tool . ProtocolTool . OutputSchema . Value . GetProperty ( "type" ) . GetString ( ) ) ;
590+ Assert . True ( tool . ProtocolTool . OutputSchema . Value . TryGetProperty ( "properties" , out var props ) ) ;
591+ Assert . True ( props . TryGetProperty ( "Name" , out _ ) ) ;
592+ Assert . True ( props . TryGetProperty ( "Age" , out _ ) ) ;
593+ }
594+
595+ [ Fact ]
596+ public async Task OutputSchemaType_ViaAttribute_ReturningCallToolResult_WorksCorrectly ( )
597+ {
598+ JsonSerializerOptions serOpts = new ( ) { TypeInfoResolver = new DefaultJsonTypeInfoResolver ( ) } ;
599+ McpServerTool tool = McpServerTool . Create (
600+ typeof ( OutputSchemaTypeTools ) . GetMethod ( nameof ( OutputSchemaTypeTools . ToolReturningCallToolResultWithSchemaType ) ) ! ,
601+ target : null ,
602+ new ( ) { SerializerOptions = serOpts } ) ;
603+
604+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
605+
606+ Mock < McpServer > srv = new ( ) ;
607+ var ctx = new RequestContext < CallToolRequestParams > ( srv . Object , CreateTestJsonRpcRequest ( ) )
608+ {
609+ Params = new CallToolRequestParams { Name = "tool_returning_call_tool_result_with_schema_type" } ,
610+ } ;
611+
612+ var toolResult = await tool . InvokeAsync ( ctx , TestContext . Current . CancellationToken ) ;
613+ Assert . Equal ( "hello" , Assert . IsType < TextContentBlock > ( toolResult . Content [ 0 ] ) . Text ) ;
614+ }
615+
616+ [ Fact ]
617+ public void OutputSchemaType_ViaAttribute_WithUseStructuredContent_ProducesExpectedSchema ( )
618+ {
619+ JsonSerializerOptions serOpts = new ( ) { TypeInfoResolver = new DefaultJsonTypeInfoResolver ( ) } ;
620+ McpServerTool tool = McpServerTool . Create (
621+ typeof ( OutputSchemaTypeTools ) . GetMethod ( nameof ( OutputSchemaTypeTools . ToolWithBothOutputSchemaTypeAndStructuredContent ) ) ! ,
622+ target : null ,
623+ new ( ) { SerializerOptions = serOpts } ) ;
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 props ) ) ;
628+ Assert . True ( props . TryGetProperty ( "Name" , out _ ) ) ;
629+ }
630+
631+ [ Fact ]
632+ public void OutputSchema_ViaOptions_OverridesAttributeOutputSchemaType ( )
633+ {
634+ var customDoc = JsonDocument . Parse ( """{"type":"object","properties":{"overridden":{"type":"string"}}}""" ) ;
635+ McpServerTool tool = McpServerTool . Create (
636+ typeof ( OutputSchemaTypeTools ) . GetMethod ( nameof ( OutputSchemaTypeTools . ToolReturningCallToolResultWithSchemaType ) ) ! ,
637+ target : null ,
638+ new ( ) { OutputSchema = customDoc . RootElement } ) ;
639+
640+ Assert . NotNull ( tool . ProtocolTool . OutputSchema ) ;
641+ Assert . True ( JsonElement . DeepEquals ( customDoc . RootElement , tool . ProtocolTool . OutputSchema . Value ) ) ;
642+ }
643+
644+ [ Fact ]
645+ public void OutputSchema_IsPreservedWhenCopyingOptions ( )
646+ {
647+ var schemaDoc = JsonDocument . Parse ( """{"type":"object","properties":{"x":{"type":"integer"}}}""" ) ;
648+
649+ // Verify OutputSchema works correctly when used via tool creation (which clones internally)
650+ McpServerTool tool1 = McpServerTool . Create ( ( ) => new CallToolResult ( ) { Content = [ ] } , new ( )
651+ {
652+ OutputSchema = schemaDoc . RootElement ,
653+ } ) ;
654+ McpServerTool tool2 = McpServerTool . Create ( ( ) => new CallToolResult ( ) { Content = [ ] } , new ( )
655+ {
656+ OutputSchema = schemaDoc . RootElement ,
657+ } ) ;
658+
659+ Assert . NotNull ( tool1 . ProtocolTool . OutputSchema ) ;
660+ Assert . NotNull ( tool2 . ProtocolTool . OutputSchema ) ;
661+ Assert . True ( JsonElement . DeepEquals ( tool1 . ProtocolTool . OutputSchema . Value , tool2 . ProtocolTool . OutputSchema . Value ) ) ;
662+ }
663+
664+ private class OutputSchemaTypeTools
665+ {
666+ [ McpServerTool ( OutputSchemaType = typeof ( Person ) ) ]
667+ public static CallToolResult ToolReturningCallToolResultWithSchemaType ( )
668+ => new ( ) { Content = [ new TextContentBlock { Text = "hello" } ] } ;
669+
670+ [ McpServerTool ( UseStructuredContent = true , OutputSchemaType = typeof ( Person ) ) ]
671+ public static CallToolResult ToolWithBothOutputSchemaTypeAndStructuredContent ( )
672+ => new ( ) { Content = [ new TextContentBlock { Text = "world" } ] } ;
673+ }
674+
675+
536676 [ Theory ]
537677 [ InlineData ( JsonNumberHandling . Strict ) ]
538678 [ InlineData ( JsonNumberHandling . AllowReadingFromString ) ]
0 commit comments