Add structured output/output schema support for server-side tools. (#480)
* Add structured output/output schema support for server-side tools. * Address feedback. * Add McpServerToolAttribute.UseStructuredContent --------- Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
parent
46d25b7558
commit
fa017c04c3
1 changed files with 310 additions and 20 deletions
|
|
@ -7,6 +7,7 @@ using System.ComponentModel;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ namespace ModelContextProtocol.Server;
|
|||
internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly bool _structuredOutputRequiresWrapping;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
|
|
@ -176,7 +178,8 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
{
|
||||
Name = options?.Name ?? function.Name,
|
||||
Description = options?.Description ?? function.Description,
|
||||
InputSchema = function.JsonSchema,
|
||||
InputSchema = function.JsonSchema,
|
||||
OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping),
|
||||
};
|
||||
|
||||
if (options is not null)
|
||||
|
|
@ -198,7 +201,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
}
|
||||
}
|
||||
|
||||
return new AIFunctionMcpServerTool(function, tool, options?.Services);
|
||||
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping);
|
||||
}
|
||||
|
||||
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
|
||||
|
|
@ -229,6 +232,8 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
{
|
||||
newOptions.ReadOnly ??= readOnly;
|
||||
}
|
||||
|
||||
newOptions.UseStructuredContent = toolAttr.UseStructuredContent;
|
||||
}
|
||||
|
||||
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
|
|
@ -243,11 +248,12 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
|
||||
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider)
|
||||
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolTool = tool;
|
||||
_logger = serviceProvider?.GetService<ILoggerFactory>()?.CreateLogger<McpServerTool>() ?? (ILogger)NullLogger.Instance;
|
||||
_structuredOutputRequiresWrapping = structuredOutputRequiresWrapping;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -295,39 +301,46 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
};
|
||||
}
|
||||
|
||||
JsonNode? structuredContent = CreateStructuredResponse(result);
|
||||
return result switch
|
||||
{
|
||||
AIContent aiContent => new()
|
||||
{
|
||||
Content = [aiContent.ToContent()],
|
||||
StructuredContent = structuredContent,
|
||||
IsError = aiContent is ErrorContent
|
||||
},
|
||||
|
||||
null => new()
|
||||
{
|
||||
Content = []
|
||||
Content = [],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
string text => new()
|
||||
{
|
||||
Content = [new() { Text = text, Type = "text" }]
|
||||
Content = [new() { Text = text, Type = "text" }],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
Content content => new()
|
||||
{
|
||||
Content = [content]
|
||||
Content = [content],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
IEnumerable<string> texts => new()
|
||||
{
|
||||
Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })]
|
||||
Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
IEnumerable<AIContent> contentItems => ConvertAIContentEnumerableToCallToolResponse(contentItems),
|
||||
IEnumerable<AIContent> contentItems => ConvertAIContentEnumerableToCallToolResponse(contentItems, structuredContent),
|
||||
|
||||
IEnumerable<Content> contents => new()
|
||||
{
|
||||
Content = [.. contents]
|
||||
Content = [.. contents],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
CallToolResponse callToolResponse => callToolResponse,
|
||||
|
|
@ -338,12 +351,90 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
{
|
||||
Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
|
||||
Type = "text"
|
||||
}]
|
||||
}],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEnumerable<AIContent> contentItems)
|
||||
private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping)
|
||||
{
|
||||
structuredOutputRequiresWrapping = false;
|
||||
|
||||
if (toolCreateOptions?.UseStructuredContent is not true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (function.GetReturnSchema(toolCreateOptions?.SchemaCreateOptions) is not JsonElement outputSchema)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (outputSchema.ValueKind is not JsonValueKind.Object ||
|
||||
!outputSchema.TryGetProperty("type", out JsonElement typeProperty) ||
|
||||
typeProperty.ValueKind is not JsonValueKind.String ||
|
||||
typeProperty.GetString() is not "object")
|
||||
{
|
||||
// If the output schema is not an object, need to modify to be a valid MCP output schema.
|
||||
JsonNode? schemaNode = JsonSerializer.SerializeToNode(outputSchema, McpJsonUtilities.JsonContext.Default.JsonElement);
|
||||
|
||||
if (schemaNode is JsonObject objSchema &&
|
||||
objSchema.TryGetPropertyValue("type", out JsonNode? typeNode) &&
|
||||
typeNode is JsonArray { Count: 2 } typeArray && typeArray.Any(type => (string?)type is "object") && typeArray.Any(type => (string?)type is "null"))
|
||||
{
|
||||
// For schemas that are of type ["object", "null"], replace with just "object" to be conformant.
|
||||
objSchema["type"] = "object";
|
||||
}
|
||||
else
|
||||
{
|
||||
// For anything else, wrap the schema in an envelope with a "result" property.
|
||||
schemaNode = new JsonObject
|
||||
{
|
||||
["type"] = "object",
|
||||
["properties"] = new JsonObject
|
||||
{
|
||||
["result"] = schemaNode
|
||||
},
|
||||
["required"] = new JsonArray { (JsonNode)"result" }
|
||||
};
|
||||
|
||||
structuredOutputRequiresWrapping = true;
|
||||
}
|
||||
|
||||
outputSchema = JsonSerializer.Deserialize(schemaNode, McpJsonUtilities.JsonContext.Default.JsonElement);
|
||||
}
|
||||
|
||||
return outputSchema;
|
||||
}
|
||||
|
||||
private JsonNode? CreateStructuredResponse(object? aiFunctionResult)
|
||||
{
|
||||
if (ProtocolTool.OutputSchema is null)
|
||||
{
|
||||
// Only provide structured responses if the tool has an output schema defined.
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode? nodeResult = aiFunctionResult switch
|
||||
{
|
||||
JsonNode node => node,
|
||||
JsonElement jsonElement => JsonSerializer.SerializeToNode(jsonElement, McpJsonUtilities.JsonContext.Default.JsonElement),
|
||||
_ => JsonSerializer.SerializeToNode(aiFunctionResult, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
|
||||
};
|
||||
|
||||
if (_structuredOutputRequiresWrapping)
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["result"] = nodeResult
|
||||
};
|
||||
}
|
||||
|
||||
return nodeResult;
|
||||
}
|
||||
|
||||
private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEnumerable<AIContent> contentItems, JsonNode? structuredContent)
|
||||
{
|
||||
List<Content> contentList = [];
|
||||
bool allErrorContent = true;
|
||||
|
|
@ -363,6 +454,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
return new()
|
||||
{
|
||||
Content = contentList,
|
||||
StructuredContent = structuredContent,
|
||||
IsError = allErrorContent && hasAny
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue