Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult#1272
Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult#1272
Conversation
…pServerToolCreateOptions - Added Type? OutputSchemaType property to McpServerToolAttribute - Added JsonElement? OutputSchema property to McpServerToolCreateOptions - Updated DeriveOptions to generate schema from OutputSchemaType via AIJsonUtilities.CreateJsonSchema - Updated CreateOutputSchema to use explicit OutputSchema when provided (takes precedence) - OutputSchema forces structured content behavior even if UseStructuredContent is false - Updated Clone() to preserve OutputSchema - Added 8 new tests covering various scenarios - Updated XML documentation Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
Outdated
Show resolved
Hide resolved
src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
…namicallyAccessedMembers - In DeriveOptions, force UseStructuredContent=true when OutputSchemaType is set - Simplify GetToolDescription to only check UseStructuredContent (no longer needs OutputSchema check) - Remove unnecessary [DynamicallyAccessedMembers] from OutputSchemaType property Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
Outdated
Show resolved
Hide resolved
…tests, add using for JsonDocument - Add Debug.Assert in CreateOutputSchema to verify UseStructuredContent is true when OutputSchema is set - Force UseStructuredContent=true in DeriveOptions when OutputSchema is set via options - Update docs to say "to a non-null value" for UseStructuredContent auto-enable - Add using statements for all JsonDocument.Parse calls in tests - Rewrite attribute-based tests to use lambdas instead of reflection - Add test for OutputSchemaType with UseStructuredContent=false to verify it's overridden - Remove OutputSchemaTypeTools helper class (no longer needed) Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
|
@copilot Please give a short explanation of the feature added in this PR from the perspective of a user of the SDK. |
…g tests - Replace OutputSchemaType attribute tests with CallToolResult<T> unit tests - Add 8 new unit tests for CallToolResult<T> in McpServerToolTests - Add 6 new integration tests in CallToolResultOfTTests - Refactor CreateOutputSchema to run wrapping logic on explicit OutputSchema too - All 1465 core tests + 267 AspNetCore tests + 57 analyzer tests pass Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Implemented in dcfff8e and b109728:
|
Use result.Content[^1] instead of result.Content[0] for both error message extraction and text content fallback deserialization. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
| // Force UseStructuredContent when OutputSchema is explicitly provided | ||
| if (newOptions.OutputSchema is not null) | ||
| { | ||
| newOptions.UseStructuredContent = true; |
There was a problem hiding this comment.
@copilot remove UseStructuredOutput on the options now. We can just use the presence of the output schema to indicate whether structured output should be used (keep the bool on the attribute)
There was a problem hiding this comment.
Done in 6db5287 — removed UseStructuredContent from McpServerToolCreateOptions. Structured output is now determined by the presence of OutputSchema on the options. The bool remains on McpServerToolAttribute; when set, DeriveOptions generates the OutputSchema from the method's return type.
…rverTool Replace the ICallToolResultTyped interface (which contained serialization logic) with a minimal ICallToolResultTypedContent interface that only exposes raw content data. The serialization/conversion logic now lives in AIFunctionMcpServerTool's ConvertCallToolResultOfT method where it's consumed. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Use the presence of OutputSchema to indicate structured output. Keep UseStructuredContent on the attribute — when set, DeriveOptions generates the OutputSchema from the method's return type. Tests updated accordingly. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
| /// the SDK to handle serialization of a strongly-typed result. | ||
| /// </para> | ||
| /// </remarks> | ||
| public sealed class CallToolResult<T> : ICallToolResultTypedContent |
There was a problem hiding this comment.
@copilot revert back to the previous version of the attribute that creates a CallToolResult
There was a problem hiding this comment.
Done in 84dcb46 — reverted to ICallToolResultTyped with ToCallToolResult(JsonSerializerOptions) that creates the CallToolResult. Removed the data-only ICallToolResultTypedContent interface and ConvertCallToolResultOfT helper.
Restore the previous interface design where CallToolResult<T> implements ICallToolResultTyped.ToCallToolResult(JsonSerializerOptions) with the serialization logic inside the generic type. Remove the data-only ICallToolResultTypedContent interface and ConvertCallToolResultOfT helper. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
| var typeInfo = serializerOptions.GetTypeInfo(typeof(T)); | ||
|
|
||
| string json = JsonSerializer.Serialize(Content, typeInfo); | ||
| JsonNode? structuredContent = JsonSerializer.SerializeToNode(Content, typeInfo); |
There was a problem hiding this comment.
@copilot we shouldn't need to serialize twice. You can serialize once to JsonNode and then ToString it
There was a problem hiding this comment.
Done in 17c3e9f — now serializes once to JsonNode via SerializeToNode, then uses ToJsonString() for the text content.
| // If the method returns CallToolResult<T>, automatically use T for the output schema | ||
| if (GetCallToolResultContentType(method) is { } contentType) | ||
| { | ||
| newOptions.OutputSchema ??= AIJsonUtilities.CreateJsonSchema(contentType, |
There was a problem hiding this comment.
@copilot if UseStructuredContent was true, won't the output schema already be set to an incorrect schema based on method.ReturnType? Seems like all of this needs to be restructured?
There was a problem hiding this comment.
Fixed in 17c3e9f — the UseStructuredContent path now calls GetCallToolResultContentType(method.ReturnType) ?? method.ReturnType so it uses T instead of CallToolResult<T> for schema generation.
|
|
||
| CallToolResult callToolResponse => callToolResponse, | ||
|
|
||
| _ when result is ICallToolResultTyped typed => typed.ToCallToolResult(AIFunction.JsonSerializerOptions), |
| Type t = method.ReturnType; | ||
|
|
||
| // Unwrap Task<T> or ValueTask<T> | ||
| if (t.IsGenericType) |
There was a problem hiding this comment.
@copilot were already doing such unwrapping elsewhere... Can this handling be done in the same place as that and consolidated?
There was a problem hiding this comment.
Consolidated in 17c3e9f — GetCallToolResultContentType now takes a Type parameter and is called from both the UseStructuredContent path (to extract T for schema generation) and the CallToolResult<T> auto-detection path.
| /// <summary> | ||
| /// Ensures the schema is a valid MCP output schema (type "object"). Wraps non-object schemas in an envelope. | ||
| /// </summary> | ||
| private static JsonElement EnsureObjectSchema(JsonElement outputSchema, ref bool structuredOutputRequiresWrapping) |
There was a problem hiding this comment.
@copilot why did you refactor this into a helper? I'd prefer to minimize the diff and only make necessary changes.
There was a problem hiding this comment.
Reverted in 17c3e9f — CreateOutputSchema is back to the original structure with inline logic. Removed the EnsureObjectSchema helper.
…minimize diff - Serialize to JsonNode once, use ToJsonString for text (avoid double serialization) - Fix UseStructuredContent + CallToolResult<T>: use T for schema, not full return type - Simplify pattern match: ICallToolResultTyped typed => instead of _ when - Change GetCallToolResultContentType to accept Type instead of MethodInfo - Revert CreateOutputSchema back to original structure (remove EnsureObjectSchema helper) Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This PR adds three main features:
[McpServerTool]
public static CallToolResult<MyResponse> MyTool(string input)
{
return new() { Content = new MyResponse { Name = "test", Score = 42 } };
}
MyResponse result = await client.CallToolAsync<MyResponse>("my_tool", args);
|
JsonElement? OutputSchemaproperty toMcpServerToolCreateOptionsOutputSchemaTypefromMcpServerToolAttribute(keepOutputSchemaon options)CallToolResult<T>class as a peer ofCallToolResultwithT ContentAIFunctionMcpServerToolto recognizeCallToolResult<T>return typeCallToolAsync<T>client method that deserializesStructuredContent/ContentasTMcpServerToolandMcpServerToolAttributeCallToolResult<T>inMcpServerToolTestsCallToolResultOfTTestsCallToolAsync<T>ICallToolResultTypedwithToCallToolResultconversion methodUseStructuredContentfromMcpServerToolCreateOptions, useOutputSchemapresence insteadT(notCallToolResult<T>) whenUseStructuredContentis setICallToolResultTyped typed =>CreateOutputSchemato original structure (remove EnsureObjectSchema helper)Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.