Set useResultSchema on mcpToolTrigger bindings for rich return types#2554
Set useResultSchema on mcpToolTrigger bindings for rich return types#2554ahmedmuhsin wants to merge 1 commit intomicrosoft:developfrom
Conversation
0bd8840 to
03b6d2b
Compare
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.
03b6d2b to
fa97ae7
Compare
| if (implementsInterface(elemClass, MCP_CONTENT_INTERFACE_FQCN)) { | ||
| return true; | ||
| } | ||
| if (isSubclassOfRichType(elemClass)) { |
There was a problem hiding this comment.
Do we also need to check whether elemClass hasAnnotationByFqcn
There was a problem hiding this comment.
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=trueonMcpToolTriggerbindings when appropriate. - Invoke the new inspection logic during configuration generation.
- Bump
azure-functions-maven-pluginfrom1.41.0to1.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.
| private static final Set<String> RICH_RESULT_TYPE_FQCNS = Set.of( | ||
| "com.microsoft.azure.functions.mcp.McpToolResult", | ||
| "io.modelcontextprotocol.spec.McpSchema.CallToolResult" | ||
| ); |
There was a problem hiding this comment.
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(...)))).
| .filter(b -> b.getBindingEnum() == BindingEnum.McpToolTrigger) | ||
| .findFirst(); | ||
|
|
||
| if (mcpToolTrigger.isEmpty()) { |
There was a problem hiding this comment.
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)).
| if (mcpToolTrigger.isEmpty()) { | |
| if (!mcpToolTrigger.isPresent()) { |
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| * Checks whether a class implements or extends any known rich result type by walking | ||
| * both the superclass chain and the implemented interfaces. |
There was a problem hiding this comment.
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.
| * 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. |
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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).
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
AnnotationHandlerImpl.java
azure-functions-maven-plugin/pom.xml
Design
Related PRs