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
10 changed files with 310 additions and 20 deletions
|
|
@ -75,5 +75,6 @@
|
|||
<PackageVersion Include="xunit.v3" Version="2.0.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.0" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="JsonSchema.Net" Version="7.3.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -24,7 +24,6 @@
|
|||
<File Path="logo.png" />
|
||||
<File Path="nuget.config" />
|
||||
<File Path="README.MD" />
|
||||
<File Path="version.json" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<File Path="src/Directory.Build.props" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
|
@ -75,6 +76,30 @@ public static partial class McpJsonUtilities
|
|||
return false; // No type keyword found.
|
||||
}
|
||||
|
||||
internal static JsonElement? GetReturnSchema(this AIFunction function, AIJsonSchemaCreateOptions? schemaCreateOptions)
|
||||
{
|
||||
// TODO replace with https://github.com/dotnet/extensions/pull/6447 once merged.
|
||||
if (function.UnderlyingMethod?.ReturnType is not Type returnType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (returnType == typeof(void) || returnType == typeof(Task) || returnType == typeof(ValueTask))
|
||||
{
|
||||
// Do not report an output schema for void or Task methods.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() is Type genericTypeDef &&
|
||||
(genericTypeDef == typeof(Task<>) || genericTypeDef == typeof(ValueTask<>)))
|
||||
{
|
||||
// Extract the real type from Task<T> or ValueTask<T> if applicable.
|
||||
returnType = returnType.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
return AIJsonUtilities.CreateJsonSchema(returnType, serializerOptions: function.JsonSerializerOptions, inferenceOptions: schemaCreateOptions);
|
||||
}
|
||||
|
||||
// Keep in sync with CreateDefaultOptions above.
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
|
@ -27,6 +28,12 @@ public class CallToolResponse
|
|||
[JsonPropertyName("content")]
|
||||
public List<Content> Content { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional JSON object representing the structured result of the tool call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("structuredContent")]
|
||||
public JsonNode? StructuredContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an indication of whether the tool call was unsuccessful.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// </summary>
|
||||
public class Tool
|
||||
{
|
||||
private JsonElement _inputSchema = McpJsonUtilities.DefaultMcpToolSchema;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the tool.
|
||||
/// </summary>
|
||||
|
|
@ -53,15 +51,44 @@ public class Tool
|
|||
[JsonPropertyName("inputSchema")]
|
||||
public JsonElement InputSchema
|
||||
{
|
||||
get => _inputSchema;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
|
||||
{
|
||||
throw new ArgumentException("The specified document is not a valid MCP tool JSON schema.", nameof(InputSchema));
|
||||
throw new ArgumentException("The specified document is not a valid MCP tool input JSON schema.", nameof(InputSchema));
|
||||
}
|
||||
|
||||
_inputSchema = value;
|
||||
field = value;
|
||||
}
|
||||
|
||||
} = McpJsonUtilities.DefaultMcpToolSchema;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a JSON Schema object defining the expected structured outputs for the tool.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The schema must be a valid JSON Schema object with the "type" property set to "object".
|
||||
/// This is enforced by validation in the setter which will throw an <see cref="ArgumentException"/>
|
||||
/// if an invalid schema is provided.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The schema should describe the shape of the data as returned in <see cref="CallToolResponse.StructuredContent"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonPropertyName("outputSchema")]
|
||||
public JsonElement? OutputSchema
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (value is not null && !McpJsonUtilities.IsValidMcpToolSchema(value.Value))
|
||||
{
|
||||
throw new ArgumentException("The specified document is not a valid MCP tool output JSON schema.", nameof(OutputSchema));
|
||||
}
|
||||
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,4 +240,13 @@ public sealed class McpServerToolAttribute : Attribute
|
|||
get => _readOnly ?? ReadOnlyDefault;
|
||||
set => _readOnly = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the tool should report an output schema for structured content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When enabled, the tool will attempt to populate the <see cref="Tool.OutputSchema"/>
|
||||
/// and provide structured content in the <see cref="CallToolResponse.StructuredContent"/> property.
|
||||
/// </remarks>
|
||||
public bool UseStructuredContent { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ public sealed class McpServerToolCreateOptions
|
|||
/// Gets or sets optional services used in the construction of the <see cref="McpServerTool"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These services will be used to determine which parameters should be satisifed from dependency injection. As such,
|
||||
/// These services will be used to determine which parameters should be satisfied from dependency injection. As such,
|
||||
/// what services are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
|
||||
/// </remarks>
|
||||
public IServiceProvider? Services { get; set; }
|
||||
|
|
@ -124,6 +125,15 @@ public sealed class McpServerToolCreateOptions
|
|||
/// </remarks>
|
||||
public bool? ReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the tool should report an output schema for structured content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When enabled, the tool will attempt to populate the <see cref="Tool.OutputSchema"/>
|
||||
/// and provide structured content in the <see cref="CallToolResponse.StructuredContent"/> property.
|
||||
/// </remarks>
|
||||
public bool UseStructuredContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JSON serializer options to use when marshalling data to/from JSON.
|
||||
/// </summary>
|
||||
|
|
@ -154,6 +164,7 @@ public sealed class McpServerToolCreateOptions
|
|||
Idempotent = Idempotent,
|
||||
OpenWorld = OpenWorld,
|
||||
ReadOnly = ReadOnly,
|
||||
UseStructuredContent = UseStructuredContent,
|
||||
SerializerOptions = SerializerOptions,
|
||||
SchemaCreateOptions = SchemaCreateOptions,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@
|
|||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="OpenTelemetry" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Json.Schema;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
|
@ -7,7 +8,10 @@ using ModelContextProtocol.Tests.Utils;
|
|||
using Moq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace ModelContextProtocol.Tests.Server;
|
||||
|
||||
|
|
@ -422,6 +426,94 @@ public partial class McpServerToolTests
|
|||
Assert.Equal(exceptionMessage, errorLog.Exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(StructuredOutput_ReturnsExpectedSchema_Inputs))]
|
||||
public async Task StructuredOutput_Enabled_ReturnsExpectedSchema<T>(T value)
|
||||
{
|
||||
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
|
||||
McpServerTool tool = McpServerTool.Create(() => value, new() { Name = "tool", UseStructuredContent = true, SerializerOptions = options });
|
||||
var mockServer = new Mock<IMcpServer>();
|
||||
var request = new RequestContext<CallToolRequestParams>(mockServer.Object)
|
||||
{
|
||||
Params = new CallToolRequestParams { Name = "tool" },
|
||||
};
|
||||
|
||||
var result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(tool.ProtocolTool.OutputSchema);
|
||||
Assert.Equal("object", tool.ProtocolTool.OutputSchema.Value.GetProperty("type").GetString());
|
||||
Assert.NotNull(result.StructuredContent);
|
||||
AssertMatchesJsonSchema(tool.ProtocolTool.OutputSchema.Value, result.StructuredContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StructuredOutput_Enabled_VoidReturningTools_ReturnsExpectedSchema()
|
||||
{
|
||||
McpServerTool tool = McpServerTool.Create(() => { });
|
||||
var mockServer = new Mock<IMcpServer>();
|
||||
var request = new RequestContext<CallToolRequestParams>(mockServer.Object)
|
||||
{
|
||||
Params = new CallToolRequestParams { Name = "tool" },
|
||||
};
|
||||
|
||||
var result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||
Assert.Null(result.StructuredContent);
|
||||
|
||||
tool = McpServerTool.Create(() => Task.CompletedTask);
|
||||
request = new RequestContext<CallToolRequestParams>(mockServer.Object)
|
||||
{
|
||||
Params = new CallToolRequestParams { Name = "tool" },
|
||||
};
|
||||
|
||||
result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||
Assert.Null(result.StructuredContent);
|
||||
|
||||
tool = McpServerTool.Create(() => ValueTask.CompletedTask);
|
||||
request = new RequestContext<CallToolRequestParams>(mockServer.Object)
|
||||
{
|
||||
Params = new CallToolRequestParams { Name = "tool" },
|
||||
};
|
||||
|
||||
result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||
Assert.Null(result.StructuredContent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(StructuredOutput_ReturnsExpectedSchema_Inputs))]
|
||||
public async Task StructuredOutput_Disabled_ReturnsExpectedSchema<T>(T value)
|
||||
{
|
||||
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
|
||||
McpServerTool tool = McpServerTool.Create(() => value, new() { UseStructuredContent = false, SerializerOptions = options });
|
||||
var mockServer = new Mock<IMcpServer>();
|
||||
var request = new RequestContext<CallToolRequestParams>(mockServer.Object)
|
||||
{
|
||||
Params = new CallToolRequestParams { Name = "tool" },
|
||||
};
|
||||
|
||||
var result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||
Assert.Null(result.StructuredContent);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> StructuredOutput_ReturnsExpectedSchema_Inputs()
|
||||
{
|
||||
yield return new object[] { "string" };
|
||||
yield return new object[] { 42 };
|
||||
yield return new object[] { 3.14 };
|
||||
yield return new object[] { true };
|
||||
yield return new object[] { new object() };
|
||||
yield return new object[] { new List<string> { "item1", "item2" } };
|
||||
yield return new object[] { new Dictionary<string, int> { ["key1"] = 1, ["key2"] = 2 } };
|
||||
yield return new object[] { new Person("John", 27) };
|
||||
}
|
||||
|
||||
private sealed class MyService;
|
||||
|
||||
private class DisposableToolType : IDisposable
|
||||
|
|
@ -510,9 +602,35 @@ public partial class McpServerToolTests
|
|||
}
|
||||
}
|
||||
|
||||
private static void AssertMatchesJsonSchema(JsonElement schemaDoc, JsonNode? value)
|
||||
{
|
||||
JsonSchema schema = JsonSerializer.Deserialize(schemaDoc, JsonContext2.Default.JsonSchema)!;
|
||||
EvaluationOptions options = new() { OutputFormat = OutputFormat.List };
|
||||
EvaluationResults results = schema.Evaluate(value, options);
|
||||
if (!results.IsValid)
|
||||
{
|
||||
IEnumerable<string> errors = results.Details
|
||||
.Where(d => d.HasErrors)
|
||||
.SelectMany(d => d.Errors!.Select(error => $"Path:${d.InstanceLocation} {error.Key}:{error.Value}"));
|
||||
|
||||
throw new XunitException($"""
|
||||
Instance JSON document does not match the specified schema.
|
||||
Schema:
|
||||
{JsonSerializer.Serialize(schema)}
|
||||
Instance:
|
||||
{value?.ToJsonString() ?? "null"}
|
||||
Errors:
|
||||
{string.Join(Environment.NewLine, errors)}
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
record Person(string Name, int Age);
|
||||
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
[JsonSerializable(typeof(DisposableToolType))]
|
||||
[JsonSerializable(typeof(AsyncDisposableToolType))]
|
||||
[JsonSerializable(typeof(AsyncDisposableAndDisposableToolType))]
|
||||
[JsonSerializable(typeof(JsonSchema))]
|
||||
partial class JsonContext2 : JsonSerializerContext;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue