Skip to content

Commit 3b643d6

Browse files
stephentoubCopilot
andcommitted
Replace CallToolResult<T> with OutputSchemaType on McpServerToolAttribute
Remove CallToolResult<T> and ICallToolResultTyped - they conflated the success type with the error type, making it impossible to provide meaningful error messages when IsError is true and T is not string. Add OutputSchemaType (Type?) property to McpServerToolAttribute, which decouples the output schema type from the return type. Methods can return T directly (throw for errors) or CallToolResult for full control over error messages, while OutputSchemaType independently specifies the schema. Also update CallToolAsync<T> to concatenate all TextContentBlock texts (with newline separator) for error messages instead of using only the first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 76ccbec commit 3b643d6

File tree

7 files changed

+72
-353
lines changed

7 files changed

+72
-353
lines changed

src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,8 +950,10 @@ public ValueTask<CallToolResult> CallToolAsync(
950950

951951
if (result.IsError is true)
952952
{
953-
string errorMessage = result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text ?? "Tool call failed.";
954-
throw new McpException(errorMessage);
953+
string errorMessage = string.Join(
954+
"\n",
955+
result.Content.OfType<TextContentBlock>().Select(c => c.Text));
956+
throw new McpException(errorMessage.Length > 0 ? errorMessage : "Tool call failed.");
955957
}
956958

957959
var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;

src/ModelContextProtocol.Core/Protocol/CallToolResultOfT.cs

Lines changed: 0 additions & 78 deletions
This file was deleted.

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
174174
{
175175
McpServerToolCreateOptions newOptions = options?.Clone() ?? new();
176176

177+
Type? outputSchemaType = null;
177178
if (method.GetCustomAttribute<McpServerToolAttribute>() is { } toolAttr)
178179
{
179180
newOptions.Name ??= toolAttr.Name;
@@ -214,6 +215,8 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
214215
{
215216
newOptions.UseStructuredContent = true;
216217
}
218+
219+
outputSchemaType = toolAttr.OutputSchemaType;
217220
}
218221

219222
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
@@ -224,10 +227,8 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
224227
// Set metadata if not already provided
225228
newOptions.Metadata ??= CreateMetadata(method);
226229

227-
// Generate the output schema from the return type if needed.
228-
// UseStructuredContent on the attribute or options uses T from CallToolResult<T> or the return type directly.
229-
// CallToolResult<T> return types automatically infer the schema from T even without UseStructuredContent.
230-
Type? outputSchemaType = GetCallToolResultContentType(method.ReturnType);
230+
// Generate the output schema from the specified type or the return type.
231+
// Priority: OutputSchemaType (explicit) > UseStructuredContent (infer from return type).
231232
if (outputSchemaType is null && newOptions.UseStructuredContent)
232233
{
233234
outputSchemaType = method.ReturnType;
@@ -323,8 +324,6 @@ public override async ValueTask<CallToolResult> InvokeAsync(
323324

324325
CallToolResult callToolResponse => callToolResponse,
325326

326-
ICallToolResultTyped typed => typed.ToCallToolResult(AIFunction.JsonSerializerOptions),
327-
328327
_ => new()
329328
{
330329
Content = [new TextContentBlock { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }],
@@ -381,28 +380,6 @@ private static bool IsAsyncMethod(MethodInfo method)
381380
return false;
382381
}
383382

384-
/// <summary>
385-
/// If the specified type is <see cref="CallToolResult{T}"/> (possibly wrapped in <see cref="Task{TResult}"/>
386-
/// or <see cref="ValueTask{TResult}"/>), returns the <c>T</c> type argument. Otherwise, returns <see langword="null"/>.
387-
/// </summary>
388-
private static Type? GetCallToolResultContentType(Type returnType)
389-
{
390-
if (returnType.IsGenericType)
391-
{
392-
Type genericDef = returnType.GetGenericTypeDefinition();
393-
if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>))
394-
{
395-
returnType = returnType.GetGenericArguments()[0];
396-
}
397-
}
398-
399-
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(CallToolResult<>))
400-
{
401-
return returnType.GetGenericArguments()[0];
402-
}
403-
404-
return null;
405-
}
406383

407384
/// <summary>Creates metadata from attributes on the specified method and its declaring class, with the MethodInfo as the first item.</summary>
408385
internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)

src/ModelContextProtocol.Core/Server/McpServerTool.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,10 @@ namespace ModelContextProtocol.Server;
142142
/// <description>Returned directly without modification.</description>
143143
/// </item>
144144
/// <item>
145-
/// <term><see cref="CallToolResult{T}"/></term>
146-
/// <description>
147-
/// The <c>T</c> content is serialized to JSON and used as both a <see cref="TextContentBlock"/>
148-
/// and as the <see cref="CallToolResult.StructuredContent"/>. The <see cref="CallToolResult{T}.IsError"/>
149-
/// and <see cref="Result.Meta"/> properties are propagated to the resulting <see cref="CallToolResult"/>.
150-
/// The <c>T</c> type argument is also used to infer the <see cref="Tool.OutputSchema"/>.
151-
/// </description>
152-
/// </item>
153-
/// <item>
154145
/// <term>Other types</term>
155-
/// <description>Serialized to JSON and returned as a single <see cref="ContentBlock"/> object with <see cref="ContentBlock.Type"/> set to "text".</description>
146+
/// <description>Serialized to JSON and returned as a single <see cref="ContentBlock"/> object with <see cref="ContentBlock.Type"/> set to "text".
147+
/// When <see cref="McpServerToolAttribute.UseStructuredContent"/> is enabled, the serialized value is also set as <see cref="CallToolResult.StructuredContent"/>
148+
/// and used to generate the <see cref="Tool.OutputSchema"/>.</description>
156149
/// </item>
157150
/// </list>
158151
/// </remarks>

src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,10 @@ namespace ModelContextProtocol.Server;
138138
/// <description>Returned directly without modification.</description>
139139
/// </item>
140140
/// <item>
141-
/// <term><see cref="CallToolResult{T}"/></term>
142-
/// <description>
143-
/// The <c>T</c> content is serialized to JSON and used as both a <see cref="TextContentBlock"/>
144-
/// and as the <see cref="CallToolResult.StructuredContent"/>. The <see cref="CallToolResult{T}.IsError"/>
145-
/// and <see cref="Result.Meta"/> properties are propagated to the resulting <see cref="CallToolResult"/>.
146-
/// The <c>T</c> type argument is also used to infer the <see cref="Tool.OutputSchema"/>.
147-
/// </description>
148-
/// </item>
149-
/// <item>
150141
/// <term>Other types</term>
151-
/// <description>Serialized to JSON and returned as a single <see cref="ContentBlock"/> object with <see cref="ContentBlock.Type"/> set to "text".</description>
142+
/// <description>Serialized to JSON and returned as a single <see cref="ContentBlock"/> object with <see cref="ContentBlock.Type"/> set to "text".
143+
/// When <see cref="UseStructuredContent"/> is enabled, the serialized value is also set as <see cref="CallToolResult.StructuredContent"/>
144+
/// and used to generate the <see cref="Tool.OutputSchema"/>.</description>
152145
/// </item>
153146
/// </list>
154147
/// </remarks>
@@ -269,11 +262,42 @@ public bool ReadOnly
269262
/// The default is <see langword="false"/>.
270263
/// </value>
271264
/// <remarks>
265+
/// <para>
272266
/// When enabled, the tool will attempt to populate the <see cref="Tool.OutputSchema"/>
273267
/// and provide structured content in the <see cref="CallToolResult.StructuredContent"/> property.
268+
/// </para>
269+
/// <para>
270+
/// Setting <see cref="OutputSchemaType"/> will automatically enable structured content.
271+
/// </para>
274272
/// </remarks>
275273
public bool UseStructuredContent { get; set; }
276274

275+
/// <summary>
276+
/// Gets or sets the type to use for generating the tool's <see cref="Tool.OutputSchema"/>.
277+
/// </summary>
278+
/// <value>
279+
/// The default is <see langword="null"/>.
280+
/// </value>
281+
/// <remarks>
282+
/// <para>
283+
/// When set, the specified type is used to generate a JSON Schema that is advertised as the tool's
284+
/// <see cref="Tool.OutputSchema"/>. This is particularly useful when the method returns
285+
/// <see cref="CallToolResult"/> directly (for example, to control <see cref="CallToolResult.IsError"/>),
286+
/// but the tool should still advertise a meaningful output schema describing the shape of
287+
/// <see cref="CallToolResult.StructuredContent"/>.
288+
/// </para>
289+
/// <para>
290+
/// Setting this property automatically enables structured content behavior, equivalent to
291+
/// setting <see cref="UseStructuredContent"/> to <see langword="true"/>.
292+
/// </para>
293+
/// <para>
294+
/// For Native AOT scenarios, use <see cref="McpServerToolCreateOptions.OutputSchema"/> to provide
295+
/// a pre-generated JSON Schema instead.
296+
/// </para>
297+
/// </remarks>
298+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
299+
public Type? OutputSchemaType { get; set; }
300+
277301
/// <summary>
278302
/// Gets or sets the source URI for the tool's icon.
279303
/// </summary>

0 commit comments

Comments
 (0)