-
Notifications
You must be signed in to change notification settings - Fork 645
Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult #1272
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 13 commits
301550d
b111284
5c82239
444dcfa
a6b7ef5
1d9f40c
dcfff8e
b109728
dd3e73b
8930269
6db5287
84dcb46
17c3e9f
f3d00f0
24bbac3
c45278b
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 |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| using System.Text.Json; | ||
| using System.Text.Json.Nodes; | ||
|
|
||
| namespace ModelContextProtocol.Protocol; | ||
|
|
||
| /// <summary> | ||
| /// Represents a strongly-typed result of a <see cref="RequestMethods.ToolsCall"/> request. | ||
| /// </summary> | ||
| /// <typeparam name="T"> | ||
| /// The type of the structured content returned by the tool. This type is used to infer the | ||
| /// <see cref="Tool.OutputSchema"/> advertised by the tool. | ||
| /// </typeparam> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// <see cref="CallToolResult{T}"/> provides a way to return strongly-typed structured content from a tool | ||
| /// while still providing access to <see cref="Result.Meta"/> and <see cref="IsError"/>. When a tool method | ||
| /// returns <see cref="CallToolResult{T}"/>, the SDK uses <typeparamref name="T"/> to infer the output schema | ||
| /// and serializes <see cref="Content"/> as both the text content and structured content of the response. | ||
| /// </para> | ||
| /// <para> | ||
| /// This type is a peer of <see cref="CallToolResult"/>, not a subclass. Use <see cref="CallToolResult"/> when | ||
| /// you need full control over individual content blocks, and <see cref="CallToolResult{T}"/> when you want | ||
| /// the SDK to handle serialization of a strongly-typed result. | ||
| /// </para> | ||
| /// </remarks> | ||
| public sealed class CallToolResult<T> : ICallToolResultTyped | ||
stephentoub marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
jeffhandley marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| /// <summary> | ||
| /// Gets or sets the typed content returned by the tool. | ||
| /// </summary> | ||
| public T? Content { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value that indicates whether the tool call was unsuccessful. | ||
| /// </summary> | ||
| /// <value> | ||
| /// <see langword="true"/> to signify that the tool execution failed; <see langword="false"/> if it was successful. | ||
| /// </value> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Tool execution errors (including input validation errors, API failures, and business logic errors) | ||
| /// are reported with this property set to <see langword="true"/> and details in the <see cref="Content"/> | ||
| /// property, rather than as protocol-level errors. | ||
| /// </para> | ||
| /// <para> | ||
| /// This design allows language models to receive detailed error feedback and potentially self-correct | ||
| /// in subsequent requests. | ||
| /// </para> | ||
| /// </remarks> | ||
| public bool? IsError { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets metadata reserved by MCP for protocol-level metadata. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Implementations must not make assumptions about its contents. | ||
| /// </remarks> | ||
| public JsonObject? Meta { get; set; } | ||
|
|
||
| /// <inheritdoc/> | ||
| CallToolResult ICallToolResultTyped.ToCallToolResult(JsonSerializerOptions serializerOptions) | ||
| { | ||
| JsonNode? structuredContent = JsonSerializer.SerializeToNode(Content, serializerOptions.GetTypeInfo(typeof(T))); | ||
|
|
||
| return new() | ||
| { | ||
| Content = [new TextContentBlock { Text = structuredContent?.ToJsonString(serializerOptions) ?? "null" }], | ||
stephentoub marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| StructuredContent = structuredContent, | ||
| IsError = IsError, | ||
| Meta = Meta, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Internal interface for converting strongly-typed tool results to <see cref="CallToolResult"/>. | ||
| /// </summary> | ||
| internal interface ICallToolResultTyped | ||
| { | ||
| /// <summary> | ||
| /// Converts the strongly-typed result to a <see cref="CallToolResult"/>. | ||
| /// </summary> | ||
| CallToolResult ToCallToolResult(JsonSerializerOptions serializerOptions); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -204,13 +204,21 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe | |
| newOptions.Icons = [new() { Source = iconSource }]; | ||
| } | ||
|
|
||
| newOptions.UseStructuredContent = toolAttr.UseStructuredContent; | ||
|
|
||
| if (toolAttr._taskSupport is { } taskSupport) | ||
| { | ||
| newOptions.Execution ??= new ToolExecution(); | ||
| newOptions.Execution.TaskSupport ??= taskSupport; | ||
| } | ||
|
|
||
| // When the attribute enables structured content, generate the output schema from the return type. | ||
| // If the return type is CallToolResult<T>, use T rather than the full return type. | ||
| if (toolAttr.UseStructuredContent) | ||
| { | ||
| Type outputType = GetCallToolResultContentType(method.ReturnType) ?? method.ReturnType; | ||
| newOptions.OutputSchema ??= AIJsonUtilities.CreateJsonSchema(outputType, | ||
| serializerOptions: newOptions.SerializerOptions ?? McpJsonUtilities.DefaultOptions, | ||
| inferenceOptions: newOptions.SchemaCreateOptions); | ||
| } | ||
| } | ||
|
|
||
| if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr) | ||
|
|
@@ -221,6 +229,14 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe | |
| // Set metadata if not already provided | ||
| newOptions.Metadata ??= CreateMetadata(method); | ||
|
|
||
| // If the method returns CallToolResult<T>, automatically use T for the output schema | ||
| if (GetCallToolResultContentType(method.ReturnType) is { } contentType) | ||
| { | ||
| newOptions.OutputSchema ??= AIJsonUtilities.CreateJsonSchema(contentType, | ||
stephentoub marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| serializerOptions: newOptions.SerializerOptions ?? McpJsonUtilities.DefaultOptions, | ||
| inferenceOptions: newOptions.SchemaCreateOptions); | ||
| } | ||
|
|
||
| return newOptions; | ||
| } | ||
|
|
||
|
|
@@ -305,6 +321,8 @@ public override async ValueTask<CallToolResult> InvokeAsync( | |
|
|
||
| CallToolResult callToolResponse => callToolResponse, | ||
|
|
||
| ICallToolResultTyped typed => typed.ToCallToolResult(AIFunction.JsonSerializerOptions), | ||
|
|
||
| _ => new() | ||
| { | ||
| Content = [new TextContentBlock { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }], | ||
|
|
@@ -361,6 +379,29 @@ private static bool IsAsyncMethod(MethodInfo method) | |
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// If the specified type is <see cref="CallToolResult{T}"/> (possibly wrapped in <see cref="Task{TResult}"/> | ||
| /// or <see cref="ValueTask{TResult}"/>), returns the <c>T</c> type argument. Otherwise, returns <see langword="null"/>. | ||
| /// </summary> | ||
| private static Type? GetCallToolResultContentType(Type returnType) | ||
| { | ||
| if (returnType.IsGenericType) | ||
|
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. @copilot why can't all of this schema handling for CallToolResult be done where the rest of the schema detection is done? There's already code unwrapping tasks, creating return schema for the return type, etc. I'm missing why this needs to be done separately.
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. Consolidated in 24bbac3 — unified into a single block after all attribute handling: Type? outputSchemaType = GetCallToolResultContentType(method.ReturnType);
if (outputSchemaType is null && useStructuredContent)
{
outputSchemaType = method.ReturnType;
}
if (outputSchemaType is not null)
{
newOptions.OutputSchema ??= AIJsonUtilities.CreateJsonSchema(outputSchemaType, ...);
}
|
||
| { | ||
| Type genericDef = returnType.GetGenericTypeDefinition(); | ||
| if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>)) | ||
| { | ||
| returnType = returnType.GetGenericArguments()[0]; | ||
| } | ||
| } | ||
|
|
||
| if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(CallToolResult<>)) | ||
| { | ||
| return returnType.GetGenericArguments()[0]; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /// <summary>Creates metadata from attributes on the specified method and its declaring class, with the MethodInfo as the first item.</summary> | ||
| internal static IReadOnlyList<object> CreateMetadata(MethodInfo method) | ||
| { | ||
|
|
@@ -432,16 +473,16 @@ private static void ValidateToolName(string name) | |
| /// 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, | ||
| /// When an output schema is present, the return description is included in the output schema. | ||
| /// When no output schema is present (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) | ||
| // If structured content is enabled (output schema present), the return description will be in the output schema | ||
| if (options?.OutputSchema is not null) | ||
| { | ||
| return description; | ||
| } | ||
|
|
@@ -483,12 +524,7 @@ schema.ValueKind is not JsonValueKind.Object || | |
| { | ||
| structuredOutputRequiresWrapping = false; | ||
|
|
||
| if (toolCreateOptions?.UseStructuredContent is not true) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| if (function.ReturnJsonSchema is not JsonElement outputSchema) | ||
| if (toolCreateOptions?.OutputSchema is not JsonElement outputSchema) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.