7
7
using System . Diagnostics . CodeAnalysis ;
8
8
using System . Reflection ;
9
9
using System . Text . Json ;
10
+ using System . Text . Json . Nodes ;
10
11
11
12
namespace ModelContextProtocol . Server ;
12
13
13
14
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
14
15
internal sealed partial class AIFunctionMcpServerTool : McpServerTool
15
16
{
16
17
private readonly ILogger _logger ;
18
+ private readonly bool _structuredOutputRequiresWrapping ;
17
19
18
20
/// <summary>
19
21
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
@@ -176,7 +178,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
176
178
{
177
179
Name = options ? . Name ?? function . Name ,
178
180
Description = options ? . Description ?? function . Description ,
179
- InputSchema = function . JsonSchema ,
181
+ InputSchema = function . JsonSchema ,
182
+ OutputSchema = CreateOutputSchema ( function , options , out bool structuredOutputRequiresWrapping ) ,
180
183
} ;
181
184
182
185
if ( options is not null )
@@ -198,7 +201,7 @@ options.OpenWorld is not null ||
198
201
}
199
202
}
200
203
201
- return new AIFunctionMcpServerTool ( function , tool , options ? . Services ) ;
204
+ return new AIFunctionMcpServerTool ( function , tool , options ? . Services , structuredOutputRequiresWrapping ) ;
202
205
}
203
206
204
207
private static McpServerToolCreateOptions DeriveOptions ( MethodInfo method , McpServerToolCreateOptions ? options )
@@ -229,6 +232,8 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
229
232
{
230
233
newOptions . ReadOnly ??= readOnly ;
231
234
}
235
+
236
+ newOptions . UseStructuredContent = toolAttr . UseStructuredContent ;
232
237
}
233
238
234
239
if ( method . GetCustomAttribute < DescriptionAttribute > ( ) is { } descAttr )
@@ -243,11 +248,12 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
243
248
internal AIFunction AIFunction { get ; }
244
249
245
250
/// <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 )
247
252
{
248
253
AIFunction = function ;
249
254
ProtocolTool = tool ;
250
255
_logger = serviceProvider ? . GetService < ILoggerFactory > ( ) ? . CreateLogger < McpServerTool > ( ) ?? ( ILogger ) NullLogger . Instance ;
256
+ _structuredOutputRequiresWrapping = structuredOutputRequiresWrapping ;
251
257
}
252
258
253
259
/// <inheritdoc />
@@ -295,39 +301,46 @@ public override async ValueTask<CallToolResponse> InvokeAsync(
295
301
} ;
296
302
}
297
303
304
+ JsonNode ? structuredContent = CreateStructuredResponse ( result ) ;
298
305
return result switch
299
306
{
300
307
AIContent aiContent => new ( )
301
308
{
302
309
Content = [ aiContent . ToContent ( ) ] ,
310
+ StructuredContent = structuredContent ,
303
311
IsError = aiContent is ErrorContent
304
312
} ,
305
313
306
314
null => new ( )
307
315
{
308
- Content = [ ]
316
+ Content = [ ] ,
317
+ StructuredContent = structuredContent ,
309
318
} ,
310
319
311
320
string text => new ( )
312
321
{
313
- Content = [ new ( ) { Text = text , Type = "text" } ]
322
+ Content = [ new ( ) { Text = text , Type = "text" } ] ,
323
+ StructuredContent = structuredContent ,
314
324
} ,
315
325
316
326
Content content => new ( )
317
327
{
318
- Content = [ content ]
328
+ Content = [ content ] ,
329
+ StructuredContent = structuredContent ,
319
330
} ,
320
331
321
332
IEnumerable < string > texts => new ( )
322
333
{
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 ,
324
336
} ,
325
337
326
- IEnumerable < AIContent > contentItems => ConvertAIContentEnumerableToCallToolResponse ( contentItems ) ,
338
+ IEnumerable < AIContent > contentItems => ConvertAIContentEnumerableToCallToolResponse ( contentItems , structuredContent ) ,
327
339
328
340
IEnumerable < Content > contents => new ( )
329
341
{
330
- Content = [ .. contents ]
342
+ Content = [ .. contents ] ,
343
+ StructuredContent = structuredContent ,
331
344
} ,
332
345
333
346
CallToolResponse callToolResponse => callToolResponse ,
@@ -338,12 +351,90 @@ public override async ValueTask<CallToolResponse> InvokeAsync(
338
351
{
339
352
Text = JsonSerializer . Serialize ( result , AIFunction . JsonSerializerOptions . GetTypeInfo ( typeof ( object ) ) ) ,
340
353
Type = "text"
341
- } ]
354
+ } ] ,
355
+ StructuredContent = structuredContent ,
342
356
} ,
343
357
} ;
344
358
}
345
359
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 )
347
438
{
348
439
List < Content > contentList = [ ] ;
349
440
bool allErrorContent = true ;
@@ -363,6 +454,7 @@ private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEn
363
454
return new ( )
364
455
{
365
456
Content = contentList ,
457
+ StructuredContent = structuredContent ,
366
458
IsError = allErrorContent && hasAny
367
459
} ;
368
460
}
0 commit comments