Skip to content

Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult#1272

Open
Copilot wants to merge 13 commits intomainfrom
copilot/support-output-schema-independently
Open

Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult#1272
Copilot wants to merge 13 commits intomainfrom
copilot/support-output-schema-independently

Conversation

Copy link
Contributor

Copilot AI commented Feb 14, 2026

  • Add JsonElement? OutputSchema property to McpServerToolCreateOptions
  • Remove OutputSchemaType from McpServerToolAttribute (keep OutputSchema on options)
  • Add sealed CallToolResult<T> class as a peer of CallToolResult with T Content
  • Update AIFunctionMcpServerTool to recognize CallToolResult<T> return type
  • Add CallToolAsync<T> client method that deserializes StructuredContent/Content as T
  • Update XML doc comments on McpServerTool and McpServerToolAttribute
  • 8 unit tests for CallToolResult<T> in McpServerToolTests
  • 6 integration tests in CallToolResultOfTTests
  • Use last content block instead of first in CallToolAsync<T>
  • Revert to ICallToolResultTyped with ToCallToolResult conversion method
  • Remove UseStructuredContent from McpServerToolCreateOptions, use OutputSchema presence instead
  • Serialize once (JsonNode) then ToString, avoid double serialization
  • Fix schema inference: use T (not CallToolResult<T>) when UseStructuredContent is set
  • Simplify pattern match to ICallToolResultTyped typed =>
  • Revert CreateOutputSchema to original structure (remove EnsureObjectSchema helper)
  • Full test suite passes
Original prompt

This section details on the original issue you should resolve

<issue_title>Support specifying OutputSchema type independently of return type for tools returning CallToolResult</issue_title>
<issue_description>Is your feature request related to a problem? Please describe.
When a tool method returns CallToolResult directly (to control properties like Meta, IsError, or manually populate StructuredContent), the SDK cannot generate a meaningful OutputSchema for the tool. The output schema either reflects the shape of CallToolResult itself—which is an internal protocol type and useless to clients—or is omitted entirely.

This forces developers to choose between:

  • Returning a strongly-typed object to get a proper OutputSchema, but losing control over Meta, IsError, and other CallToolResult properties.
  • Returning CallToolResult for full response control, but with no way to advertise the actual shape of StructuredContent to clients.

Describe the solution you'd like
Allow developers to specify the output schema type independently of the return type, for example via an attribute parameter or a create option:

// Option A: Attribute-based
[McpServerTool(UseStructuredContent = true, OutputSchemaType = typeof(MyResponseType))]
public static CallToolResult my_tool(string input) { ... }



// Option B: Via McpServerToolCreateOptions
McpServerToolCreateOptions options = new()
{
    UseStructuredContent = true,
    OutputSchemaType = typeof(MyResponseType),
};

When OutputSchemaType is set, the SDK would generate the OutputSchema from that type while still allowing the method to return CallToolResult with full control over the response.

Describe alternatives you've considered

  • Returning a typed object: Works for simple cases, but precludes setting Meta, IsError, or customizing Content independently of StructuredContent.
  • Schema node transformation workaround: Creating a stub tool with McpServerTool.Create(() => new T()) solely to extract its output schema, then using SchemaCreateOptions.TransformSchemaNode on the real tool to replace the CallToolResult schema with the stub's. This works but is fragile, non-obvious, and requires significant boilerplate.
  • Manually setting StructuredContent without UseStructuredContent: Provides structured data to clients but no OutputSchema is advertised in tools/list, so clients can't validate or understand the response shape ahead of time.

Additional context
This gap is particularly impactful in scenarios where tools need to return both structured content and out-of-band metadata via CallToolResult.Meta. 
</issue_description>

<agent_instructions>Add a Type? OutputSchemaType property to McpServerToolAttribute and a JsonElement? OutputSchema property to McpServerToolCreateOptions. The OutputSchema property, if it exists, will just be used to initialize the OutputSchema on the creation options using AIJsonUtilities. This let's someone using the attribute specify a type, and someone using the options bag directly either specify a type (with a call to AIJsonUtilities to create the schema) or just supply their own custom schema. If the schema is specified, UseStructuredOutput should be forced to true in the options bag. Make sure to add good test coverage of this in the existing test files. Also ensure any existing docs about UseStructuredOutput and [McpServerTool] are updated appropriately.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 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.

…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>
Copilot AI changed the title [WIP] Support specifying OutputSchema type for CallToolResult Add OutputSchemaType and OutputSchema for tools returning CallToolResult Feb 14, 2026
Copilot AI requested a review from stephentoub February 14, 2026 13:53
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>
Copy link
Contributor

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…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>
@stephentoub stephentoub enabled auto-merge (squash) February 14, 2026 22:52
@mikekistler
Copy link
Contributor

@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>
Copy link
Contributor Author

Copilot AI commented Feb 18, 2026

Should we add a CallToolResult<T> where we use T as the OutputSchemaType?

YES!
...

Implemented in dcfff8e and b109728:

  • Removed OutputSchemaType from McpServerToolAttribute (kept OutputSchema on McpServerToolCreateOptions)
  • Added sealed CallToolResult<T> in CallToolResultOfT.cs — peer of CallToolResult with T? Content, bool? IsError, and JsonObject? Meta
  • AIFunctionMcpServerTool detects CallToolResult<T> return type (including Task<> / ValueTask<> wrappers), uses T to infer OutputSchema, and handles conversion via ICallToolResultTyped internal interface
  • Added CallToolAsync<T> on McpClient — deserializes StructuredContent (or falls back to text content) as T, throws McpException if IsError is true
  • Updated XML docs on McpServerTool and McpServerToolAttribute
  • 8 unit tests + 6 integration tests, full suite passes

Copilot AI changed the title Add OutputSchemaType and OutputSchema for tools returning CallToolResult Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult Feb 18, 2026
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;
Copy link
Contributor

@stephentoub stephentoub Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI changed the title Add CallToolResult<T>, CallToolAsync<T>, and OutputSchema for tools returning CallToolResult Add CallToolResult&lt;T&gt;, CallToolAsync&lt;T&gt;, and OutputSchema for tools returning CallToolResult Feb 18, 2026
/// the SDK to handle serialization of a strongly-typed result.
/// </para>
/// </remarks>
public sealed class CallToolResult<T> : ICallToolResultTypedContent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot revert back to the previous version of the attribute that creates a CallToolResult

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we shouldn't need to serialize twice. You can serialize once to JsonNode and then ToString it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why the _? Why not just ICallToolResultTyped typed => ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17c3e9f — changed to ICallToolResultTyped typed =>.

Type t = method.ReturnType;

// Unwrap Task<T> or ValueTask<T>
if (t.IsGenericType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot were already doing such unwrapping elsewhere... Can this handling be done in the same place as that and consolidated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated in 17c3e9fGetCallToolResultContentType 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why did you refactor this into a helper? I'd prefer to minimize the diff and only make necessary changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in 17c3e9fCreateOutputSchema 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>
Copy link
Contributor Author

Copilot AI commented Feb 18, 2026

@copilot Please give a short explanation of the feature added in this PR from the perspective of a user of the SDK.

This PR adds three main features:

  1. CallToolResult<T> — a new return type for tool methods that gives you both structured content and control over IsError/Meta. The SDK infers the output schema from T:
[McpServerTool]
public static CallToolResult<MyResponse> MyTool(string input)
{
    return new() { Content = new MyResponse { Name = "test", Score = 42 } };
}
  1. CallToolAsync<T> — a client-side method that calls a tool and deserializes the result as T:
MyResponse result = await client.CallToolAsync<MyResponse>("my_tool", args);
  1. McpServerToolCreateOptions.OutputSchema — for advanced scenarios where you want to supply a custom JSON schema directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support specifying OutputSchema type independently of return type for tools returning CallToolResult

4 participants