Add azure-functions-java-mcp module for rich content and structured content support#49
Add azure-functions-java-mcp module for rich content and structured content support#49ahmedmuhsin wants to merge 10 commits intoAzure:devfrom
Conversation
New module providing: - McpToolResultMiddleware: SPI middleware for automatic result wrapping - McpToolResult: Result envelope with factory methods for rich content - McpContent annotation: Marks POJOs for dual text + structured content - README with setup, usage examples, and JSON impl documentation Uses official MCP Java SDK types (io.modelcontextprotocol.sdk:mcp-core) with McpJsonDefaults.getMapper() for serialization. Supports both mcp-json-jackson2 and mcp-json-jackson3 via provided scope. Requires Java 17+ (MCP SDK uses sealed interfaces and records).
- Add license headers to all Java files - Declare local variables as final - Move operators to new lines (OperatorWrap) - Remove unused import (java.util.Map) - Fix import ordering (CustomImportOrder)
- Revert CI to Java 11 for existing modules (avoids spotbugs/Groovy compat issues) - Add dedicated Java 17 CI step to build azure-functions-java-mcp only - Use JDK profile in reactor POM to skip MCP module under Java 11 - Revert spotbugs overrides from all pre-existing module POMs - Revert bat file changes (no longer needed) - Keep spotbugs 4.8.3.1 override in MCP module only (built with Java 17)
TsuyoshiUshio
left a comment
There was a problem hiding this comment.
Review: Add azure-functions-java-mcp module
This is a well-designed module — the SPI middleware approach with auto-discovery is clean, the TCCL workaround is spot-on, and the defensive error handling ensures the middleware won't break existing functions. A few items below.
Suggestion: serializeContentList() — manual JSON array construction is unnecessary
The current implementation manually builds a JSON array with StringBuilder:
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < contentBlocks.size(); i++) {
if (i > 0) sb.append(",");
sb.append(mapper.writeValueAsString(contentBlocks.get(i)));
}
sb.append("]");However, McpJsonMapper.writeValueAsString(Object value) accepts any Object, including List. Since the MCP SDK's Jackson configuration already handles polymorphic serialization of Content subtypes (preserving the "type" discriminator in each element), you can simplify this to:
private static String serializeContentList(List<? extends Content> contentBlocks) throws IOException {
return McpJsonDefaults.getMapper().writeValueAsString(contentBlocks);
}This is safer (no risk of malformed JSON from manual concatenation) and more maintainable. If there was a specific reason for element-by-element serialization (e.g., a polymorphic serialization issue observed during testing), please document it in a code comment.
Concern: Gson/Jackson dual-serializer — document the implications for @McpContent users
McpToolResult uses Jackson (via McpJsonDefaults.getMapper()) for MCP SDK Content types and Gson for the envelope and @McpContent-annotated POJOs. This means:
- Jackson annotations (
@JsonProperty,@JsonIgnore) on@McpContentPOJOs will be silently ignored - Gson-specific annotations (
@SerializedName,@Expose) are what users should use if they need to customize serialization
Consider adding a note in the README under the @McpContent section, e.g.: "Serialization uses Gson — use @SerializedName for custom field names."
Concern: List<?> type check uses only the first element
if (!list.isEmpty() && list.get(0) instanceof Content) {
return McpToolResult.fromContentList((List<Content>) list);
}A mixed list (e.g., List.of(new TextContent("hi"), "not content")) would pass this check but fail during serialization. This matches the Python SDK's approach (also first-element only), so the risk is low in practice — but a brief code comment noting this intentional design choice would help future maintainers.
Nit: OpenTelemetry changes not mentioned in PR description
The PR includes checkstyle fixes in the opentelemetry module (final keywords, copyright headers). These are harmless but should be noted in the PR description for reviewer awareness — it's easy to overlook scope changes in a 13-file diff.
Nit: Missing newline at end of pom.xml
Both the parent pom.xml and build.yml are missing a trailing newline.
Praise
- TCCL workaround — capturing the customer classloader at construction time and restoring it before
McpJsonDefaults.getMapper()calls is the correct fix for the worker's classloader reset behavior. Well-documented with comments explaining why. - Defensive error handling — the
catchininvoke()logs a WARNING and falls back to the raw value. This ensures a middleware bug never breaks the customer's function. - Java 17 profile isolation — the
java17-modulesMaven profile with<jdk>[17,)</jdk>activation keeps the MCP module out of Java 11 builds automatically. - 14 unit tests with good coverage of all wrapping paths, including edge cases (empty list, string list, primitives).
- README is excellent — clear setup, usage examples for all 4 patterns, and the JSON implementation table.
There was a problem hiding this comment.
Pull request overview
Adds a new azure-functions-java-mcp module to enable MCP tool functions in Azure Functions Java to return rich content blocks and structured content (instead of plain text), with CI/build updates to support Java 17-only compilation for that module.
Changes:
- Adds
azure-functions-java-mcp(middleware + result envelope +@McpContentannotation) and registers it via SPI. - Updates the root
pom.xmlto include the new module only when building on JDK 17+. - Updates CI to build existing libraries on Java 11 and the new MCP module on Java 17; also adds license headers / minor refinements in the OpenTelemetry module.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pom.xml | Adds a JDK 17-activated profile to include the new MCP module only on Java 17+. |
| eng/ci/templates/jobs/build.yml | Builds core libraries with Java 11, then builds the MCP module with Java 17. |
| azure-functions-java-opentelemetry/src/main/java/com/microsoft/azure/functions/opentelemetry/TraceContextTextMapGetter.java | Adds license header. |
| azure-functions-java-opentelemetry/src/main/java/com/microsoft/azure/functions/opentelemetry/OpenTelemetryInvocationMiddleware.java | Adds license header and minor local variable immutability. |
| azure-functions-java-opentelemetry/src/main/java/com/microsoft/azure/functions/opentelemetry/FunctionsResourceDetector.java | Adds license header and minor local variable immutability. |
| azure-functions-java-opentelemetry/src/main/java/com/microsoft/azure/functions/opentelemetry/FunctionsOpenTelemetry.java | Adds license header and minor immutability/string concat formatting tweaks. |
| azure-functions-java-mcp/src/main/java/com/microsoft/azure/functions/mcp/McpToolResultMiddleware.java | New middleware that wraps supported MCP return values into the host extension envelope format. |
| azure-functions-java-mcp/src/main/java/com/microsoft/azure/functions/mcp/McpToolResult.java | New envelope/factory for text, content blocks, multi-content, and structured content. |
| azure-functions-java-mcp/src/main/java/com/microsoft/azure/functions/mcp/McpContent.java | New annotation marking POJOs for dual (text + structured) serialization. |
| azure-functions-java-mcp/src/main/resources/META-INF/services/com.microsoft.azure.functions.internal.spi.middleware.Middleware | Registers the middleware via ServiceLoader. |
| azure-functions-java-mcp/src/test/java/com/microsoft/azure/functions/mcp/McpToolResultMiddlewareTest.java | Unit tests for wrapping behavior and factory methods. |
| azure-functions-java-mcp/pom.xml | New module POM targeting Java 17 with MCP SDK + test dependencies. |
| azure-functions-java-mcp/README.md | Documentation and usage examples for the new module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add null check to fromContent() with IllegalArgumentException - Add null/empty checks to fromContentList() with IllegalArgumentException - Simplify serializeContentList() to single mapper.writeValueAsString() call instead of manual StringBuilder JSON construction - Add code comment on intentional first-element-only List type check in wrapReturnValue(), matching Python SDK approach - Add Gson serialization note to README @McpContent section - Add trailing newlines to pom.xml and build.yml
|
Thanks for the detailed review! Addressed all items in the latest push: 1. Simplify 2. Gson/Jackson dual-serializer docs: 3. First-element-only List check: 4. OpenTelemetry changes: 5. Missing trailing newlines: |
Summary
Adds a new azure-functions-java-mcp module that enables MCP tool functions to return rich content types (images, resource links, multi-content) and structured content, instead of plain text strings.
What's included
New module: azure-functions-java-mcp
Dependencies
Requirements
Other changes
finalkeywords, string concat formatting). No functional changes.Related PRs