Synthesize return description into tool description when UseStructuredContent is false (#1049)
This commit is contained in:
parent
ae17ba0b4b
commit
4808ca0324
2 changed files with 134 additions and 1 deletions
|
|
@ -118,7 +118,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||||
Tool tool = new()
|
Tool tool = new()
|
||||||
{
|
{
|
||||||
Name = options?.Name ?? function.Name,
|
Name = options?.Name ?? function.Name,
|
||||||
Description = options?.Description ?? function.Description,
|
Description = GetToolDescription(function, options),
|
||||||
InputSchema = function.JsonSchema,
|
InputSchema = function.JsonSchema,
|
||||||
OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping),
|
OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping),
|
||||||
Icons = options?.Icons,
|
Icons = options?.Icons,
|
||||||
|
|
@ -406,6 +406,57 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the tool description, synthesizing from both the function description and return description when appropriate.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When UseStructuredContent is true, the return description is included in the output schema.
|
||||||
|
/// When UseStructuredContent is false (default), if there's a return description in the ReturnJsonSchema,
|
||||||
|
/// it will be appended to the tool description so the information is still available to consumers.
|
||||||
|
/// </remarks>
|
||||||
|
private static string? GetToolDescription(AIFunction function, McpServerToolCreateOptions? options)
|
||||||
|
{
|
||||||
|
string? description = options?.Description ?? function.Description;
|
||||||
|
|
||||||
|
// If structured content is enabled, the return description will be in the output schema
|
||||||
|
if (options?.UseStructuredContent is true)
|
||||||
|
{
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When structured content is disabled, try to extract the return description from ReturnJsonSchema
|
||||||
|
// and append it to the tool description so the information is available to consumers
|
||||||
|
string? returnDescription = GetReturnDescription(function.ReturnJsonSchema);
|
||||||
|
if (string.IsNullOrWhiteSpace(returnDescription))
|
||||||
|
{
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synthesize a combined description
|
||||||
|
if (string.IsNullOrWhiteSpace(description))
|
||||||
|
{
|
||||||
|
return $"Returns: {returnDescription}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{description}\nReturns: {returnDescription}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the description property from a ReturnJsonSchema if present.
|
||||||
|
/// </summary>
|
||||||
|
private static string? GetReturnDescription(JsonElement? returnJsonSchema)
|
||||||
|
{
|
||||||
|
if (returnJsonSchema is not JsonElement schema ||
|
||||||
|
schema.ValueKind is not JsonValueKind.Object ||
|
||||||
|
!schema.TryGetProperty("description", out JsonElement descriptionElement) ||
|
||||||
|
descriptionElement.ValueKind is not JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptionElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping)
|
private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping)
|
||||||
{
|
{
|
||||||
structuredOutputRequiresWrapping = false;
|
structuredOutputRequiresWrapping = false;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -743,6 +744,87 @@ public partial class McpServerToolTests
|
||||||
Assert.Null(tool.ProtocolTool.Icons);
|
Assert.Null(tool.ProtocolTool.Icons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnDescription_StructuredOutputDisabled_IncludedInToolDescription()
|
||||||
|
{
|
||||||
|
// When UseStructuredContent is false (default), return description should be appended to tool description
|
||||||
|
McpServerTool tool = McpServerTool.Create(ToolWithReturnDescription);
|
||||||
|
|
||||||
|
Assert.Equal("Tool that returns data.\nReturns: The computed result", tool.ProtocolTool.Description);
|
||||||
|
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnDescription_StructuredOutputEnabled_NotIncludedInToolDescription()
|
||||||
|
{
|
||||||
|
// When UseStructuredContent is true, return description should be in the output schema, not in tool description
|
||||||
|
McpServerTool tool = McpServerTool.Create(ToolWithReturnDescription, new() { UseStructuredContent = true });
|
||||||
|
|
||||||
|
Assert.Equal("Tool that returns data.", tool.ProtocolTool.Description);
|
||||||
|
Assert.NotNull(tool.ProtocolTool.OutputSchema);
|
||||||
|
// Verify the output schema contains the description
|
||||||
|
Assert.True(tool.ProtocolTool.OutputSchema.Value.TryGetProperty("properties", out var properties));
|
||||||
|
Assert.True(properties.TryGetProperty("result", out var result));
|
||||||
|
Assert.True(result.TryGetProperty("description", out var description));
|
||||||
|
Assert.Equal("The computed result", description.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnDescription_NoFunctionDescription_OnlyReturnsDescription()
|
||||||
|
{
|
||||||
|
// When there's no function description but there's a return description
|
||||||
|
McpServerTool tool = McpServerTool.Create(ToolWithOnlyReturnDescription);
|
||||||
|
|
||||||
|
Assert.Equal("Returns: The computed result", tool.ProtocolTool.Description);
|
||||||
|
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnDescription_ExplicitDescriptionOption_SynthesizesWithReturnDescription()
|
||||||
|
{
|
||||||
|
// When Description is explicitly set in options and there's a return description,
|
||||||
|
// the return description should be appended since UseStructuredContent is false
|
||||||
|
McpServerTool tool = McpServerTool.Create(ToolWithReturnDescription, new() { Description = "Custom description" });
|
||||||
|
|
||||||
|
Assert.Equal("Custom description\nReturns: The computed result", tool.ProtocolTool.Description);
|
||||||
|
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnDescription_NoReturnDescription_NoChange()
|
||||||
|
{
|
||||||
|
// When there's no return description, the tool description should remain unchanged
|
||||||
|
McpServerTool tool = McpServerTool.Create(ToolWithoutReturnDescription);
|
||||||
|
|
||||||
|
Assert.Equal("Tool without return description.", tool.ProtocolTool.Description);
|
||||||
|
Assert.Null(tool.ProtocolTool.OutputSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnDescription_StructuredOutputEnabled_WithExplicitDescription_NoSynthesis()
|
||||||
|
{
|
||||||
|
// When UseStructuredContent is true and Description is set, return description goes to output schema
|
||||||
|
McpServerTool tool = McpServerTool.Create(ToolWithReturnDescription, new()
|
||||||
|
{
|
||||||
|
Description = "Custom description",
|
||||||
|
UseStructuredContent = true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Description should not have the return description appended
|
||||||
|
Assert.Equal("Custom description", tool.ProtocolTool.Description);
|
||||||
|
Assert.NotNull(tool.ProtocolTool.OutputSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Description("Tool that returns data.")]
|
||||||
|
[return: Description("The computed result")]
|
||||||
|
private static string ToolWithReturnDescription() => "result";
|
||||||
|
|
||||||
|
[return: Description("The computed result")]
|
||||||
|
private static string ToolWithOnlyReturnDescription() => "result";
|
||||||
|
|
||||||
|
[Description("Tool without return description.")]
|
||||||
|
private static string ToolWithoutReturnDescription() => "result";
|
||||||
|
|
||||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||||
[JsonSerializable(typeof(DisposableToolType))]
|
[JsonSerializable(typeof(DisposableToolType))]
|
||||||
[JsonSerializable(typeof(AsyncDisposableToolType))]
|
[JsonSerializable(typeof(AsyncDisposableToolType))]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue