-
Notifications
You must be signed in to change notification settings - Fork 707
Relax outputSchema to any JSON Schema 2020-12 document per SEP-2106 #1568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
126d712
cb2c315
c30725c
614f19f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,6 @@ namespace ModelContextProtocol.Server; | |
| /// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary> | ||
| internal sealed partial class AIFunctionMcpServerTool : McpServerTool | ||
| { | ||
| private readonly bool _structuredOutputRequiresWrapping; | ||
| private readonly IReadOnlyList<object> _metadata; | ||
|
|
||
| /// <summary> | ||
|
|
@@ -120,7 +119,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( | |
| Name = options?.Name ?? function.Name, | ||
| Description = GetToolDescription(function, options), | ||
| InputSchema = function.JsonSchema, | ||
| OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping), | ||
| OutputSchema = CreateOutputSchema(function, options), | ||
| Icons = options?.Icons, | ||
| }; | ||
|
|
||
|
|
@@ -167,7 +166,7 @@ options.OpenWorld is not null || | |
| tool.Execution.TaskSupport = ToolTaskSupport.Optional; | ||
| } | ||
|
|
||
| return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? []); | ||
| return new AIFunctionMcpServerTool(function, tool, options?.Services, options?.Metadata ?? []); | ||
| } | ||
|
|
||
| private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options) | ||
|
|
@@ -235,14 +234,13 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe | |
| internal AIFunction AIFunction { get; } | ||
|
|
||
| /// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary> | ||
| private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping, IReadOnlyList<object> metadata) | ||
| private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, IReadOnlyList<object> metadata) | ||
| { | ||
| ValidateToolName(tool.Name); | ||
|
|
||
| AIFunction = function; | ||
| ProtocolTool = tool; | ||
|
|
||
| _structuredOutputRequiresWrapping = structuredOutputRequiresWrapping; | ||
| _metadata = metadata; | ||
| } | ||
|
|
||
|
|
@@ -485,65 +483,27 @@ schema.ValueKind is not JsonValueKind.Object || | |
| return descriptionElement.GetString(); | ||
| } | ||
|
|
||
| private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping) | ||
| private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For backward compatibility reasons, I don't think we can completely eliminate the wrapping of non-object schemas, since this will still be needed for clients using 2025-11-25 or earlier protocol versions.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrapping for non-object schemas is restored in c30725c, just at the wire emission boundary instead of inside |
||
| { | ||
| structuredOutputRequiresWrapping = false; | ||
|
|
||
| if (toolCreateOptions?.UseStructuredContent is not true) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| // Per SEP-2106, any valid JSON Schema document is acceptable for outputSchema — | ||
| // arrays, primitives, compositions, and nullable types pass through unchanged. | ||
| // Explicit OutputSchema takes precedence over AIFunction's return schema. | ||
| JsonElement outputSchema; | ||
| if (toolCreateOptions.OutputSchema is { } explicitSchema) | ||
| { | ||
| outputSchema = explicitSchema; | ||
| } | ||
| else if (function.ReturnJsonSchema is { } returnSchema) | ||
| { | ||
| outputSchema = returnSchema; | ||
| } | ||
| else | ||
| { | ||
| return null; | ||
| return explicitSchema; | ||
| } | ||
|
|
||
| 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 (function.ReturnJsonSchema is { } returnSchema) | ||
| { | ||
| // 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 returnSchema; | ||
| } | ||
|
|
||
| return outputSchema; | ||
| return null; | ||
| } | ||
|
|
||
| private JsonElement? CreateStructuredResponse(object? aiFunctionResult) | ||
|
|
@@ -554,26 +514,15 @@ typeProperty.ValueKind is not JsonValueKind.String || | |
| return null; | ||
| } | ||
|
|
||
| JsonElement? elementResult = aiFunctionResult switch | ||
| // Per SEP-2106, the tool's return value flows through to structuredContent unchanged. | ||
| // No "result" envelope wrapping — the value's natural JSON shape is what the schema describes. | ||
| return aiFunctionResult switch | ||
| { | ||
| JsonElement jsonElement => jsonElement, | ||
| JsonNode node => JsonSerializer.SerializeToElement(node, McpJsonUtilities.JsonContext.Default.JsonNode), | ||
| null => null, | ||
| _ => JsonSerializer.SerializeToElement(aiFunctionResult, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))), | ||
| }; | ||
|
|
||
| if (_structuredOutputRequiresWrapping) | ||
| { | ||
| JsonNode? resultNode = elementResult is { } je | ||
| ? JsonSerializer.SerializeToNode(je, McpJsonUtilities.JsonContext.Default.JsonElement) | ||
| : null; | ||
| return JsonSerializer.SerializeToElement(new JsonObject | ||
| { | ||
| ["result"] = resultNode | ||
| }, McpJsonUtilities.JsonContext.Default.JsonObject); | ||
| } | ||
|
|
||
| return elementResult; | ||
| } | ||
|
|
||
| private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumerable<AIContent> contentItems, JsonElement? structuredContent) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this method should be named
IsValidToolOutputSchema, since it is designed specifically to validate tool output schemas and not any JSON Schema document.I think we may also need to make this conditional on the protocol version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed the function.
About version control here, I did not make that change to keep tools contain schema per SEP-2106. But serving of schema on
tools/callandtools/listmake the decision of wrapping depending on negotiated protocol version