Skip to content

Set useResultSchema on mcpToolTrigger bindings for rich return types#2554

Open
ahmedmuhsin wants to merge 1 commit intomicrosoft:developfrom
ahmedmuhsin:feature/mcp-structured-content
Open

Set useResultSchema on mcpToolTrigger bindings for rich return types#2554
ahmedmuhsin wants to merge 1 commit intomicrosoft:developfrom
ahmedmuhsin:feature/mcp-structured-content

Conversation

@ahmedmuhsin
Copy link
Copy Markdown
Contributor

@ahmedmuhsin ahmedmuhsin commented Mar 16, 2026

Summary

Adds return-type inspection to McpAnnotationProcessor to automatically set useResultSchema=true on mcpToolTrigger bindings in function.json when the function returns a rich content type. Also bumps azure-functions-maven-plugin version to 1.42.0.

Changes

McpAnnotationProcessor.java

  • New method setUseResultSchemaIfNeeded(Method, List of Binding) that inspects the function method's return type
  • Detects rich types by FQCN string matching: McpToolResult, MCP SDK Content types (TextContent, ImageContent, AudioContent, ResourceLink, EmbeddedResource, CallToolResult)
  • Detects @McpContent-annotated POJOs via annotation FQCN
  • Handles List of Content via generic type argument inspection
  • Checks superclass chain and implemented interfaces for subtype detection
  • Only sets the flag when there are no output bindings (matches C# McpUseResultSchemaTransformer behavior)

AnnotationHandlerImpl.java

  • One-line addition: calls McpAnnotationProcessor.setUseResultSchemaIfNeeded(method, bindings) after MCP annotation processing in generateConfiguration()

azure-functions-maven-plugin/pom.xml

  • Version bump from 1.41.0 to 1.42.0

Design

  • Uses FQCN string comparison to avoid compile-time dependencies on the MCP modules
  • The useResultSchema flag tells the host extension to use ToolReturnValueBinder (which understands the McpToolResult envelope) instead of SimpleToolReturnValueBinder (which just calls .toString())

Related PRs

Add McpAnnotationProcessor.setUseResultSchemaIfNeeded() which inspects
the function method's return type and sets useResultSchema=true on the
mcpToolTrigger binding when the return type is:
- McpToolResult (azure-functions-java-mcp)
- MCP SDK Content types (TextContent, ImageContent, ResourceLink, etc.)
- @McpContent-annotated POJOs
- List<Content> (including generic type inspection)

The flag is only set when there are no output bindings, matching the
behavior of the C# McpUseResultSchemaTransformer.

Type detection uses FQCN string matching and interface/superclass
walking to avoid compile-time dependencies on the MCP modules.
if (implementsInterface(elemClass, MCP_CONTENT_INTERFACE_FQCN)) {
return true;
}
if (isSubclassOfRichType(elemClass)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we also need to check whether elemClass hasAnnotationByFqcn

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances MCP (Model Context Protocol) Azure Functions annotation processing so that generated function.json bindings automatically enable useResultSchema=true for MCP tool triggers when a function returns rich MCP content types, aligning return-value binding behavior with the host extension’s expectations. It also updates the azure-functions-maven-plugin version.

Changes:

  • Add return-type inspection to set useResultSchema=true on McpToolTrigger bindings when appropriate.
  • Invoke the new inspection logic during configuration generation.
  • Bump azure-functions-maven-plugin from 1.41.0 to 1.42.0.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
azure-toolkit-libs/azure-toolkit-appservice-lib/src/main/java/com/microsoft/azure/toolkit/lib/legacy/function/handlers/McpAnnotationProcessor.java Adds return-type detection logic to decide when to set useResultSchema for MCP tool triggers.
azure-toolkit-libs/azure-toolkit-appservice-lib/src/main/java/com/microsoft/azure/toolkit/lib/legacy/function/handlers/AnnotationHandlerImpl.java Calls the new setUseResultSchemaIfNeeded hook during configuration generation.
azure-functions-maven-plugin/pom.xml Updates plugin version to 1.42.0.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +215 to +218
private static final Set<String> RICH_RESULT_TYPE_FQCNS = Set.of(
"com.microsoft.azure.functions.mcp.McpToolResult",
"io.modelcontextprotocol.spec.McpSchema.CallToolResult"
);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

RICH_RESULT_TYPE_FQCNS is initialized with Set.of(...), which is only available starting in Java 9. This module is configured for Java 8 (maven.compiler.source/target = 1.8), so this will not compile. Use a Java 8 compatible initialization (e.g., Collections.unmodifiableSet(new HashSet<>(Arrays.asList(...)))).

Copilot uses AI. Check for mistakes.
.filter(b -> b.getBindingEnum() == BindingEnum.McpToolTrigger)
.findFirst();

if (mcpToolTrigger.isEmpty()) {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

Optional.isEmpty() is used here, but Optional#isEmpty was added in Java 11. Since this project targets Java 8, this will not compile. Replace with !mcpToolTrigger.isPresent() (or avoid Optional entirely by using findFirst().orElse(null)).

Suggested change
if (mcpToolTrigger.isEmpty()) {
if (!mcpToolTrigger.isPresent()) {

Copilot uses AI. Check for mistakes.
Comment on lines +299 to +315
// Check List<Content> or List<? extends Content> via generic return type
if (List.class.isAssignableFrom(returnType) && genericReturnType instanceof ParameterizedType) {
final ParameterizedType pt = (ParameterizedType) genericReturnType;
final Type[] typeArgs = pt.getActualTypeArguments();
if (typeArgs.length > 0 && typeArgs[0] instanceof Class<?>) {
final Class<?> elemClass = (Class<?>) typeArgs[0];
final String elemFqcn = elemClass.getCanonicalName();
if (elemFqcn != null && RICH_RESULT_TYPE_FQCNS.contains(elemFqcn)) {
return true;
}
if (implementsInterface(elemClass, MCP_CONTENT_INTERFACE_FQCN)) {
return true;
}
if (isSubclassOfRichType(elemClass)) {
return true;
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The List<Content> or List<? extends Content> check doesn't actually handle wildcards or other non-Class<?> type arguments: typeArgs[0] will be a WildcardType for List<? extends Content>, so this branch won't detect it. To match the comment/PR intent, handle WildcardType (upper bounds) and ParameterizedType when extracting the element type.

Copilot uses AI. Check for mistakes.
Comment on lines +335 to +336
* Checks whether a class implements or extends any known rich result type by walking
* both the superclass chain and the implemented interfaces.
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The Javadoc says this method walks "both the superclass chain and the implemented interfaces", but the implementation only checks the superclass chain. Either update the comment to match the implementation or extend the method to also inspect implemented interfaces if that's required for correctness.

Suggested change
* Checks whether a class implements or extends any known rich result type by walking
* both the superclass chain and the implemented interfaces.
* Checks whether a class extends any known rich result type by walking
* up the superclass chain.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +268
public static void setUseResultSchemaIfNeeded(final Method method, final List<Binding> bindings) {
// Find the MCP tool trigger binding
final Optional<Binding> mcpToolTrigger = bindings.stream()
.filter(b -> b.getBindingEnum() == BindingEnum.McpToolTrigger)
.findFirst();

if (mcpToolTrigger.isEmpty()) {
return;
}

// Don't set useResultSchema when there are output bindings
final boolean hasOutputBindings = bindings.stream()
.anyMatch(b -> b.getBindingEnum().getDirection() == BindingEnum.Direction.OUT);
if (hasOutputBindings) {
return;
}

final Class<?> returnType = method.getReturnType();
if (returnType.equals(Void.TYPE)) {
return;
}

if (needsResultSchema(returnType, method.getGenericReturnType())) {
mcpToolTrigger.get().setAttribute("useResultSchema", true);
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

New return-type inspection logic (setUseResultSchemaIfNeeded / needsResultSchema) is not covered by unit tests. There are existing tests for McpAnnotationProcessor in this module; please add tests for the key scenarios (no output bindings vs. with output bindings; direct rich type; Content implementations; @McpContent POJO; and List<Content> / wildcard generic cases).

Copilot uses AI. Check for mistakes.
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.

3 participants