Skip to content

Add azure-functions-java-mcp module for rich content and structured content support#49

Open
ahmedmuhsin wants to merge 10 commits intoAzure:devfrom
ahmedmuhsin:feature/mcp-structured-content
Open

Add azure-functions-java-mcp module for rich content and structured content support#49
ahmedmuhsin wants to merge 10 commits intoAzure:devfrom
ahmedmuhsin:feature/mcp-structured-content

Conversation

@ahmedmuhsin
Copy link
Copy Markdown
Contributor

@ahmedmuhsin ahmedmuhsin commented Mar 16, 2026

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

  • McpToolResultMiddleware - SPI middleware auto-discovered by the Java worker. Intercepts MCP tool function return values and wraps them in the McpToolResult envelope format expected by the host extension. Handles Content types, List of Content, @McpContent-annotated POJOs, and McpToolResult pass-through. Includes a TCCL workaround for the worker's classloader reset after function execution.
  • McpToolResult - Result envelope class with factory methods (text(), fromContent(), fromContentList(), fromStructuredContent()). Uses McpJsonDefaults.getMapper() from the MCP SDK for Content serialization and Gson for the envelope and user POJOs.
  • @McpContent - Annotation for marking user POJOs that should be dual-serialized as both text content (backward compat) and structured content (for clients that support it).
  • README.md - Setup guide, usage examples for all patterns, JSON implementation documentation.
  • 14 unit tests covering all wrapping paths.

Dependencies

  • Uses official MCP Java SDK types via mcp-json-jackson2 as provided scope - user can choose jackson2 or jackson3.
  • Registered in parent pom.xml as a new module.

Requirements

  • Java 17+ (MCP Java SDK uses sealed interfaces and records)

Other changes

  • OpenTelemetry module: Added license headers and minor checkstyle fixes (final keywords, string concat formatting). No functional changes.
  • pom.xml / build.yml: Added missing trailing newlines.

Related PRs

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)
Copy link
Copy Markdown

@TsuyoshiUshio TsuyoshiUshio left a comment

Choose a reason for hiding this comment

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

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 @McpContent POJOs 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 catch in invoke() 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-modules Maven 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.

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

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 + @McpContent annotation) and registers it via SPI.
  • Updates the root pom.xml to 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
@ahmedmuhsin
Copy link
Copy Markdown
Contributor Author

ahmedmuhsin commented Apr 22, 2026

@TsuyoshiUshio

Thanks for the detailed review! Addressed all items in the latest push:

1. Simplify serializeContentList():
Agreed - replaced the manual StringBuilder construction with a single mapper.writeValueAsString(contentBlocks) call. All 14 tests pass with the same output, confirming Jackson's polymorphic config handles the list correctly.

2. Gson/Jackson dual-serializer docs:
Added a note under the @McpContent section in the README: "Note: @McpContent POJOs are serialized using Gson (not Jackson). Use @SerializedName for custom field names. Jackson annotations like @JsonProperty will be ignored."

3. First-element-only List check:
Added a code comment explaining the intentional design choice and that it matches the Python SDK's approach. The middleware's catch block already handles any serialization failures from mixed-type lists gracefully.

4. OpenTelemetry changes:
Added an "Other changes" section to the PR description noting the license headers and checkstyle fixes.

5. Missing trailing newlines:
Fixed in both pom.xml and build.yml.

Copy link
Copy Markdown

@TsuyoshiUshio TsuyoshiUshio left a comment

Choose a reason for hiding this comment

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

LGTM

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.

4 participants