@@ -324,6 +324,177 @@ public class ToolResultObject
324324 /// </summary>
325325 [ JsonPropertyName ( "toolTelemetry" ) ]
326326 public Dictionary < string , object > ? ToolTelemetry { get ; set ; }
327+
328+ /// <summary>
329+ /// Attempts to interpret the result as an MCP <c>CallToolResult</c>
330+ /// (shape: <c>{ content: [...], isError?: bool }</c>) and converts it to a
331+ /// <see cref="ToolResultObject"/>. Returns <see langword="null"/> if the value does
332+ /// not match the expected shape.
333+ /// </summary>
334+ internal static ToolResultObject ? TryConvertFromCallToolResult ( object ? result )
335+ {
336+ if ( result is not JsonElement element )
337+ {
338+ return null ;
339+ }
340+
341+ if ( element . ValueKind != JsonValueKind . Object )
342+ {
343+ return null ;
344+ }
345+
346+ if ( ! element . TryGetProperty ( "content" , out var contentProp ) || contentProp . ValueKind != JsonValueKind . Array )
347+ {
348+ return null ;
349+ }
350+
351+ // Validate every element has a string "type" field
352+ foreach ( var item in contentProp . EnumerateArray ( ) )
353+ {
354+ if ( item . ValueKind != JsonValueKind . Object ||
355+ ! item . TryGetProperty ( "type" , out var typeProp ) ||
356+ typeProp . ValueKind != JsonValueKind . String )
357+ {
358+ return null ;
359+ }
360+ }
361+
362+ List < string > ? textParts = null ;
363+ List < ToolBinaryResult > ? binaryResults = null ;
364+
365+ foreach ( var block in contentProp . EnumerateArray ( ) )
366+ {
367+ var blockType = block . GetProperty ( "type" ) . GetString ( ) ;
368+
369+ switch ( blockType )
370+ {
371+ case "text" :
372+ if ( block . TryGetProperty ( "text" , out var textProp ) && textProp . ValueKind == JsonValueKind . String )
373+ {
374+ ( textParts ??= [ ] ) . Add ( textProp . GetString ( ) ! ) ;
375+ }
376+ break ;
377+
378+ case "image" :
379+ ( binaryResults ??= [ ] ) . Add ( new ToolBinaryResult
380+ {
381+ Data = block . TryGetProperty ( "data" , out var imgData ) && imgData . ValueKind == JsonValueKind . String ? imgData . GetString ( ) ?? "" : "" ,
382+ MimeType = block . TryGetProperty ( "mimeType" , out var imgMime ) && imgMime . ValueKind == JsonValueKind . String ? imgMime . GetString ( ) ?? "" : "" ,
383+ Type = "image" ,
384+ } ) ;
385+ break ;
386+
387+ case "resource" :
388+ if ( block . TryGetProperty ( "resource" , out var resProp ) && resProp . ValueKind == JsonValueKind . Object )
389+ {
390+ if ( resProp . TryGetProperty ( "text" , out var resText ) && resText . ValueKind == JsonValueKind . String )
391+ {
392+ var text = resText . GetString ( ) ;
393+ if ( ! string . IsNullOrEmpty ( text ) )
394+ {
395+ ( textParts ??= [ ] ) . Add ( text ! ) ;
396+ }
397+ }
398+
399+ if ( resProp . TryGetProperty ( "blob" , out var resBlob ) && resBlob . ValueKind == JsonValueKind . String )
400+ {
401+ var blob = resBlob . GetString ( ) ;
402+ if ( ! string . IsNullOrEmpty ( blob ) )
403+ {
404+ var mimeType = resProp . TryGetProperty ( "mimeType" , out var resMime ) && resMime . ValueKind == JsonValueKind . String
405+ ? resMime . GetString ( ) ?? "application/octet-stream"
406+ : "application/octet-stream" ;
407+ var uri = resProp . TryGetProperty ( "uri" , out var resUri ) && resUri . ValueKind == JsonValueKind . String
408+ ? resUri . GetString ( )
409+ : null ;
410+
411+ ( binaryResults ??= [ ] ) . Add ( new ToolBinaryResult
412+ {
413+ Data = blob ! ,
414+ MimeType = mimeType ,
415+ Type = "resource" ,
416+ Description = uri ,
417+ } ) ;
418+ }
419+ }
420+ }
421+ break ;
422+ }
423+ }
424+
425+ var isError = element . TryGetProperty ( "isError" , out var isErrorProp ) &&
426+ isErrorProp . ValueKind == JsonValueKind . True ;
427+
428+ return new ToolResultObject
429+ {
430+ TextResultForLlm = textParts is not null ? string . Join ( "\n " , textParts ) : "" ,
431+ ResultType = isError ? "failure" : "success" ,
432+ BinaryResultsForLlm = binaryResults ,
433+ } ;
434+ }
435+
436+ /// <summary>
437+ /// Attempts to convert a result from an <see cref="AIFunction"/> invocation into a
438+ /// <see cref="ToolResultObject"/>. Handles <see cref="TextContent"/>,
439+ /// <see cref="DataContent"/>, and collections of <see cref="AIContent"/>.
440+ /// Returns <see langword="null"/> if the value is not a recognized <see cref="AIContent"/> type.
441+ /// </summary>
442+ internal static ToolResultObject ? TryConvertFromAIContent ( object ? result )
443+ {
444+ if ( result is AIContent singleContent )
445+ {
446+ return ConvertAIContents ( [ singleContent ] ) ;
447+ }
448+
449+ if ( result is IEnumerable < AIContent > contentList )
450+ {
451+ return ConvertAIContents ( contentList ) ;
452+ }
453+
454+ return null ;
455+ }
456+
457+ private static ToolResultObject ConvertAIContents ( IEnumerable < AIContent > contents )
458+ {
459+ List < string > ? textParts = null ;
460+ List < ToolBinaryResult > ? binaryResults = null ;
461+
462+ foreach ( var content in contents )
463+ {
464+ switch ( content )
465+ {
466+ case TextContent textContent :
467+ if ( textContent . Text is { } text )
468+ {
469+ ( textParts ??= [ ] ) . Add ( text ) ;
470+ }
471+ break ;
472+
473+ case DataContent dataContent :
474+ ( binaryResults ??= [ ] ) . Add ( new ToolBinaryResult
475+ {
476+ Data = dataContent . Base64Data . ToString ( ) ,
477+ MimeType = dataContent . MediaType ?? "application/octet-stream" ,
478+ Type = dataContent . HasTopLevelMediaType ( "image" ) ? "image" : "resource" ,
479+ } ) ;
480+ break ;
481+
482+ default :
483+ ( textParts ??= [ ] ) . Add ( SerializeAIContent ( content ) ) ;
484+ break ;
485+ }
486+ }
487+
488+ return new ToolResultObject
489+ {
490+ TextResultForLlm = textParts is not null ? string . Join ( "\n " , textParts ) : "" ,
491+ ResultType = "success" ,
492+ BinaryResultsForLlm = binaryResults ,
493+ } ;
494+ }
495+
496+ private static string SerializeAIContent ( AIContent content ) =>
497+ JsonSerializer . Serialize ( content , AIJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( AIContent ) ) ) ;
327498}
328499
329500/// <summary>
0 commit comments