Skip to content
8 changes: 1 addition & 7 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1549,13 +1549,7 @@ public async Task<ToolCallResponseV2> OnToolCallV2(string sessionId,

var result = await tool.InvokeAsync(aiFunctionArgs);

var toolResultObject = result is ToolResultAIContent trac ? trac.Result : new ToolResultObject
{
ResultType = "success",
TextResultForLlm = result is JsonElement { ValueKind: JsonValueKind.String } je
? je.GetString()!
: JsonSerializer.Serialize(result, tool.JsonSerializerOptions.GetTypeInfo(typeof(object))),
};
var toolResultObject = ToolResultObject.ConvertFromInvocationResult(result, tool.JsonSerializerOptions);
return new ToolCallResponseV2(toolResultObject);
}
catch (Exception ex)
Expand Down
8 changes: 1 addition & 7 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,7 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName,

var result = await tool.InvokeAsync(aiFunctionArgs);

var toolResultObject = result is ToolResultAIContent trac ? trac.Result : new ToolResultObject
{
ResultType = "success",
TextResultForLlm = result is JsonElement { ValueKind: JsonValueKind.String } je
? je.GetString()!
: JsonSerializer.Serialize(result, tool.JsonSerializerOptions.GetTypeInfo(typeof(object))),
};
var toolResultObject = ToolResultObject.ConvertFromInvocationResult(result, tool.JsonSerializerOptions);

await Rpc.Tools.HandlePendingToolCallAsync(requestId, toolResultObject, error: null);
}
Expand Down
89 changes: 89 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,95 @@ public class ToolResultObject
/// </summary>
[JsonPropertyName("toolTelemetry")]
public Dictionary<string, object>? ToolTelemetry { get; set; }

/// <summary>
/// Converts the result of an <see cref="AIFunction"/> invocation into a
/// <see cref="ToolResultObject"/>. Handles <see cref="ToolResultAIContent"/>,
/// <see cref="AIContent"/>, and falls back to JSON serialization.
/// </summary>
internal static ToolResultObject ConvertFromInvocationResult(object? result, JsonSerializerOptions jsonOptions)
{
if (result is ToolResultAIContent trac)
{
return trac.Result;
}

if (TryConvertFromAIContent(result) is { } aiConverted)
{
return aiConverted;
}

return new ToolResultObject
{
ResultType = "success",
TextResultForLlm = result is JsonElement { ValueKind: JsonValueKind.String } je
? je.GetString()!
: JsonSerializer.Serialize(result, jsonOptions.GetTypeInfo(typeof(object))),
};
}

/// <summary>
/// Attempts to convert a result from an <see cref="AIFunction"/> invocation into a
/// <see cref="ToolResultObject"/>. Handles <see cref="TextContent"/>,
/// <see cref="DataContent"/>, and collections of <see cref="AIContent"/>.
/// Returns <see langword="null"/> if the value is not a recognized <see cref="AIContent"/> type.
/// </summary>
internal static ToolResultObject? TryConvertFromAIContent(object? result)
{
if (result is AIContent singleContent)
{
return ConvertAIContents([singleContent]);
}

if (result is IEnumerable<AIContent> contentList)
{
return ConvertAIContents(contentList);
}

return null;
}

private static ToolResultObject ConvertAIContents(IEnumerable<AIContent> contents)
{
List<string>? textParts = null;
List<ToolBinaryResult>? binaryResults = null;

foreach (var content in contents)
{
switch (content)
{
case TextContent textContent:
if (textContent.Text is { } text)
{
(textParts ??= []).Add(text);
}
break;

case DataContent dataContent:
(binaryResults ??= []).Add(new ToolBinaryResult
{
Data = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType ?? "application/octet-stream",
Type = dataContent.HasTopLevelMediaType("image") ? "image" : "resource",
});
break;

default:
(textParts ??= []).Add(SerializeAIContent(content));
break;
}
}

return new ToolResultObject
{
TextResultForLlm = textParts is not null ? string.Join("\n", textParts) : "",
ResultType = "success",
BinaryResultsForLlm = binaryResults,
};
}

private static string SerializeAIContent(AIContent content) =>
JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AIContent)));
}

/// <summary>
Expand Down
102 changes: 101 additions & 1 deletion go/definetool.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"strings"

"github.com/google/jsonschema-go/jsonschema"
)
Expand Down Expand Up @@ -65,7 +66,8 @@ func createTypedHandler[T any, U any](handler func(T, ToolInvocation) (U, error)
}

// normalizeResult converts any value to a ToolResult.
// Strings pass through directly, ToolResult passes through, other types are JSON-serialized.
// Strings pass through directly, ToolResult passes through, and other types
// are JSON-serialized.
func normalizeResult(result any) (ToolResult, error) {
if result == nil {
return ToolResult{
Expand Down Expand Up @@ -99,6 +101,104 @@ func normalizeResult(result any) (ToolResult, error) {
}, nil
}

// ConvertMCPCallToolResult converts an MCP CallToolResult value (a map or struct
// with a "content" array and optional "isError" bool) into a ToolResult.
// Returns the converted ToolResult and true if the value matched the expected
// shape, or a zero ToolResult and false otherwise.
func ConvertMCPCallToolResult(value any) (ToolResult, bool) {
m, ok := value.(map[string]any)
if !ok {
jsonBytes, err := json.Marshal(value)
if err != nil {
return ToolResult{}, false
}

if err := json.Unmarshal(jsonBytes, &m); err != nil {
return ToolResult{}, false
}
}

contentRaw, exists := m["content"]
if !exists {
return ToolResult{}, false
}

contentSlice, ok := contentRaw.([]any)
if !ok {
return ToolResult{}, false
}

// Verify every element has a string "type" field
for _, item := range contentSlice {
block, ok := item.(map[string]any)
if !ok {
return ToolResult{}, false
}
if _, ok := block["type"].(string); !ok {
return ToolResult{}, false
}
}

var textParts []string
var binaryResults []ToolBinaryResult

for _, item := range contentSlice {
block := item.(map[string]any)
blockType := block["type"].(string)

switch blockType {
case "text":
if text, ok := block["text"].(string); ok {
textParts = append(textParts, text)
}
case "image":
data, _ := block["data"].(string)
mimeType, _ := block["mimeType"].(string)
if data == "" {
continue
}
binaryResults = append(binaryResults, ToolBinaryResult{
Data: data,
MimeType: mimeType,
Type: "image",
})
case "resource":
if resRaw, ok := block["resource"].(map[string]any); ok {
if text, ok := resRaw["text"].(string); ok && text != "" {
textParts = append(textParts, text)
}
if blob, ok := resRaw["blob"].(string); ok && blob != "" {
mimeType, _ := resRaw["mimeType"].(string)
if mimeType == "" {
mimeType = "application/octet-stream"
}
uri, _ := resRaw["uri"].(string)
binaryResults = append(binaryResults, ToolBinaryResult{
Data: blob,
MimeType: mimeType,
Type: "resource",
Description: uri,
})
}
}
}
}

resultType := "success"
if isErr, ok := m["isError"].(bool); ok && isErr {
resultType = "failure"
}

tr := ToolResult{
TextResultForLLM: strings.Join(textParts, "\n"),
ResultType: resultType,
}
if len(binaryResults) > 0 {
tr.BinaryResultsForLLM = binaryResults
}
return tr, true
}

// generateSchemaForType generates a JSON schema map from a Go type using reflection.
// Panics if schema generation fails, as this indicates a programming error.
func generateSchemaForType(t reflect.Type) map[string]any {
Expand Down
Loading
Loading