77using System . Diagnostics . CodeAnalysis ;
88using System . Reflection ;
99using System . Text . Json ;
10+ using System . Text . Json . Nodes ;
1011
1112namespace ModelContextProtocol . Server ;
1213
1314/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
1415internal sealed partial class AIFunctionMcpServerTool : McpServerTool
1516{
1617 private readonly ILogger _logger ;
18+ private readonly bool _structuredOutputRequiresWrapping ;
1719
1820 /// <summary>
1921 /// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
@@ -176,7 +178,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
176178 {
177179 Name = options ? . Name ?? function . Name ,
178180 Description = options ? . Description ?? function . Description ,
179- InputSchema = function . JsonSchema ,
181+ InputSchema = function . JsonSchema ,
182+ OutputSchema = CreateOutputSchema ( function , options , out bool structuredOutputRequiresWrapping ) ,
180183 } ;
181184
182185 if ( options is not null )
@@ -198,7 +201,7 @@ options.OpenWorld is not null ||
198201 }
199202 }
200203
201- return new AIFunctionMcpServerTool ( function , tool , options ? . Services ) ;
204+ return new AIFunctionMcpServerTool ( function , tool , options ? . Services , structuredOutputRequiresWrapping ) ;
202205 }
203206
204207 private static McpServerToolCreateOptions DeriveOptions ( MethodInfo method , McpServerToolCreateOptions ? options )
@@ -229,6 +232,8 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
229232 {
230233 newOptions . ReadOnly ??= readOnly ;
231234 }
235+
236+ newOptions . UseStructuredContent = toolAttr . UseStructuredContent ;
232237 }
233238
234239 if ( method . GetCustomAttribute < DescriptionAttribute > ( ) is { } descAttr )
@@ -243,11 +248,12 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
243248 internal AIFunction AIFunction { get ; }
244249
245250 /// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
246- private AIFunctionMcpServerTool ( AIFunction function , Tool tool , IServiceProvider ? serviceProvider )
251+ private AIFunctionMcpServerTool ( AIFunction function , Tool tool , IServiceProvider ? serviceProvider , bool structuredOutputRequiresWrapping )
247252 {
248253 AIFunction = function ;
249254 ProtocolTool = tool ;
250255 _logger = serviceProvider ? . GetService < ILoggerFactory > ( ) ? . CreateLogger < McpServerTool > ( ) ?? ( ILogger ) NullLogger . Instance ;
256+ _structuredOutputRequiresWrapping = structuredOutputRequiresWrapping ;
251257 }
252258
253259 /// <inheritdoc />
@@ -295,39 +301,46 @@ public override async ValueTask<CallToolResponse> InvokeAsync(
295301 } ;
296302 }
297303
304+ JsonNode ? structuredContent = CreateStructuredResponse ( result ) ;
298305 return result switch
299306 {
300307 AIContent aiContent => new ( )
301308 {
302309 Content = [ aiContent . ToContent ( ) ] ,
310+ StructuredContent = structuredContent ,
303311 IsError = aiContent is ErrorContent
304312 } ,
305313
306314 null => new ( )
307315 {
308- Content = [ ]
316+ Content = [ ] ,
317+ StructuredContent = structuredContent ,
309318 } ,
310319
311320 string text => new ( )
312321 {
313- Content = [ new ( ) { Text = text , Type = "text" } ]
322+ Content = [ new ( ) { Text = text , Type = "text" } ] ,
323+ StructuredContent = structuredContent ,
314324 } ,
315325
316326 Content content => new ( )
317327 {
318- Content = [ content ]
328+ Content = [ content ] ,
329+ StructuredContent = structuredContent ,
319330 } ,
320331
321332 IEnumerable < string > texts => new ( )
322333 {
323- Content = [ .. texts . Select ( x => new Content ( ) { Type = "text" , Text = x ?? string . Empty } ) ]
334+ Content = [ .. texts . Select ( x => new Content ( ) { Type = "text" , Text = x ?? string . Empty } ) ] ,
335+ StructuredContent = structuredContent ,
324336 } ,
325337
326- IEnumerable < AIContent > contentItems => ConvertAIContentEnumerableToCallToolResponse ( contentItems ) ,
338+ IEnumerable < AIContent > contentItems => ConvertAIContentEnumerableToCallToolResponse ( contentItems , structuredContent ) ,
327339
328340 IEnumerable < Content > contents => new ( )
329341 {
330- Content = [ .. contents ]
342+ Content = [ .. contents ] ,
343+ StructuredContent = structuredContent ,
331344 } ,
332345
333346 CallToolResponse callToolResponse => callToolResponse ,
@@ -338,12 +351,90 @@ public override async ValueTask<CallToolResponse> InvokeAsync(
338351 {
339352 Text = JsonSerializer . Serialize ( result , AIFunction . JsonSerializerOptions . GetTypeInfo ( typeof ( object ) ) ) ,
340353 Type = "text"
341- } ]
354+ } ] ,
355+ StructuredContent = structuredContent ,
342356 } ,
343357 } ;
344358 }
345359
346- private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse ( IEnumerable < AIContent > contentItems )
360+ private static JsonElement ? CreateOutputSchema ( AIFunction function , McpServerToolCreateOptions ? toolCreateOptions , out bool structuredOutputRequiresWrapping )
361+ {
362+ structuredOutputRequiresWrapping = false ;
363+
364+ if ( toolCreateOptions ? . UseStructuredContent is not true )
365+ {
366+ return null ;
367+ }
368+
369+ if ( function . GetReturnSchema ( toolCreateOptions ? . SchemaCreateOptions ) is not JsonElement outputSchema )
370+ {
371+ return null ;
372+ }
373+
374+ if ( outputSchema . ValueKind is not JsonValueKind . Object ||
375+ ! outputSchema . TryGetProperty ( "type" , out JsonElement typeProperty ) ||
376+ typeProperty . ValueKind is not JsonValueKind . String ||
377+ typeProperty . GetString ( ) is not "object" )
378+ {
379+ // If the output schema is not an object, need to modify to be a valid MCP output schema.
380+ JsonNode ? schemaNode = JsonSerializer . SerializeToNode ( outputSchema , McpJsonUtilities . JsonContext . Default . JsonElement ) ;
381+
382+ if ( schemaNode is JsonObject objSchema &&
383+ objSchema . TryGetPropertyValue ( "type" , out JsonNode ? typeNode ) &&
384+ typeNode is JsonArray { Count : 2 } typeArray && typeArray . Any ( type => ( string ? ) type is "object" ) && typeArray . Any ( type => ( string ? ) type is "null" ) )
385+ {
386+ // For schemas that are of type ["object", "null"], replace with just "object" to be conformant.
387+ objSchema [ "type" ] = "object" ;
388+ }
389+ else
390+ {
391+ // For anything else, wrap the schema in an envelope with a "result" property.
392+ schemaNode = new JsonObject
393+ {
394+ [ "type" ] = "object" ,
395+ [ "properties" ] = new JsonObject
396+ {
397+ [ "result" ] = schemaNode
398+ } ,
399+ [ "required" ] = new JsonArray { ( JsonNode ) "result" }
400+ } ;
401+
402+ structuredOutputRequiresWrapping = true ;
403+ }
404+
405+ outputSchema = JsonSerializer . Deserialize ( schemaNode , McpJsonUtilities . JsonContext . Default . JsonElement ) ;
406+ }
407+
408+ return outputSchema ;
409+ }
410+
411+ private JsonNode ? CreateStructuredResponse ( object ? aiFunctionResult )
412+ {
413+ if ( ProtocolTool . OutputSchema is null )
414+ {
415+ // Only provide structured responses if the tool has an output schema defined.
416+ return null ;
417+ }
418+
419+ JsonNode ? nodeResult = aiFunctionResult switch
420+ {
421+ JsonNode node => node ,
422+ JsonElement jsonElement => JsonSerializer . SerializeToNode ( jsonElement , McpJsonUtilities . JsonContext . Default . JsonElement ) ,
423+ _ => JsonSerializer . SerializeToNode ( aiFunctionResult , AIFunction . JsonSerializerOptions . GetTypeInfo ( typeof ( object ) ) ) ,
424+ } ;
425+
426+ if ( _structuredOutputRequiresWrapping )
427+ {
428+ return new JsonObject
429+ {
430+ [ "result" ] = nodeResult
431+ } ;
432+ }
433+
434+ return nodeResult ;
435+ }
436+
437+ private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse ( IEnumerable < AIContent > contentItems , JsonNode ? structuredContent )
347438 {
348439 List < Content > contentList = [ ] ;
349440 bool allErrorContent = true ;
@@ -363,6 +454,7 @@ private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEn
363454 return new ( )
364455 {
365456 Content = contentList ,
457+ StructuredContent = structuredContent ,
366458 IsError = allErrorContent && hasAny
367459 } ;
368460 }
0 commit comments