Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
package org.springaicommunity.mcp.adapter;

import java.util.List;
import java.util.Map;

import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Utils;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.method.tool.utils.JsonParser;

/**
* @author Christian Tzolov
* @author Alexandros Pappas
*/
public class ResourceAdapter {

Expand All @@ -27,7 +31,8 @@ public static McpSchema.Resource asResource(McpResource mcpResourceAnnotation) {
.name(name)
.title(mcpResourceAnnotation.title())
.description(mcpResourceAnnotation.description())
.mimeType(mcpResourceAnnotation.mimeType());
.mimeType(mcpResourceAnnotation.mimeType())
.meta(parseMeta(mcpResourceAnnotation.meta()));

// Only set annotations if not default value is provided
// This is a workaround since Java annotations do not support null default values
Expand All @@ -48,8 +53,21 @@ public static McpSchema.ResourceTemplate asResourceTemplate(McpResource mcpResou
if (name == null || name.isEmpty()) {
name = "resource"; // Default name when not specified
}
return new McpSchema.ResourceTemplate(mcpResource.uri(), name, mcpResource.description(),
mcpResource.mimeType(), null);
return McpSchema.ResourceTemplate.builder()
.uriTemplate(mcpResource.uri())
.name(name)
.description(mcpResource.description())
.mimeType(mcpResource.mimeType())
.meta(parseMeta(mcpResource.meta()))
.build();
}

@SuppressWarnings("unchecked")
private static Map<String, Object> parseMeta(String metaJson) {
if (!Utils.hasText(metaJson)) {
return null;
}
return JsonParser.fromJson(metaJson, Map.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Marks a method as a MCP Resource.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
Expand Down Expand Up @@ -50,6 +51,12 @@
*/
String mimeType() default "text/plain";

/**
* Optional JSON string representing the _meta field for this resource. The value is
* parsed as a JSON object and passed to the Resource builder's meta method.
*/
String meta() default "";

/**
* Optional annotations for the client. Note: The default annotations value is
* ignored.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

/**
* @author Christian Tzolov
* @author Alexandros Pappas
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
Expand Down Expand Up @@ -46,6 +47,12 @@
*/
String title() default "";

/**
* Optional JSON string representing the _meta field for this tool. The value is
* parsed as a JSON object and passed to the Tool builder's meta method.
*/
String meta() default "";

/**
* Additional properties describing a Tool to clients.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* and other common operations.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
public abstract class AbstractMcpResourceMethodCallback {

Expand Down Expand Up @@ -75,6 +76,8 @@ public enum ContentType {

protected final ContentType contentType;

protected final Map<String, Object> meta;

/**
* Constructor for AbstractMcpResourceMethodCallback.
* @param method The method to create a callback for
Expand All @@ -86,10 +89,11 @@ public enum ContentType {
* @param resultConverter The result converter
* @param uriTemplateMangerFactory The URI template manager factory
* @param contentType The content type
* @param meta The resource metadata to propagate to content-level _meta
*/
protected AbstractMcpResourceMethodCallback(Method method, Object bean, String uri, String name, String description,
String mimeType, McpReadResourceResultConverter resultConverter,
McpUriTemplateManagerFactory uriTemplateMangerFactory, ContentType contentType) {
McpUriTemplateManagerFactory uriTemplateMangerFactory, ContentType contentType, Map<String, Object> meta) {

Assert.hasText(uri, "URI can't be null or empty!");
Assert.notNull(method, "Method can't be null!");
Expand All @@ -109,6 +113,7 @@ protected AbstractMcpResourceMethodCallback(Method method, Object bean, String u
this.uriVariables = this.uriTemplateManager.getVariableNames();

this.contentType = contentType;
this.meta = meta;
}

/**
Expand Down Expand Up @@ -567,6 +572,8 @@ protected abstract static class AbstractBuilder<T extends AbstractBuilder<T, R>,

protected String uri; // Resource URI

protected Map<String, Object> meta; // Resource metadata

/**
* Set the method to create a callback for.
* @param method The method to create a callback for
Expand Down Expand Up @@ -609,6 +616,7 @@ public T resource(McpSchema.Resource resource) {
this.name = resource.name();
this.description = resource.description();
this.mimeType = resource.mimeType();
this.meta = resource.meta();
return (T) this;
}

Expand All @@ -622,6 +630,7 @@ public T resource(McpSchema.ResourceTemplate resourceTemplate) {
this.name = resourceTemplate.name();
this.description = resourceTemplate.description();
this.mimeType = resourceTemplate.mimeType();
this.meta = resourceTemplate.meta();
return (T) this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
* variables.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
public final class AsyncMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
implements BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> {

private AsyncMcpResourceMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType);
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
this.validateMethod(this.method);
}

Expand Down Expand Up @@ -126,13 +127,13 @@ public Mono<ReadResourceResult> apply(McpAsyncServerExchange exchange, ReadResou
if (result instanceof Mono<?>) {
// If the result is already a Mono, use it
return ((Mono<?>) result).map(r -> this.resultConverter.convertToReadResourceResult(r,
request.uri(), this.mimeType, this.contentType));
request.uri(), this.mimeType, this.contentType, this.meta));
}
else {
// Otherwise, convert the result to a ReadResourceResult and wrap in a
// Mono
return Mono.just(this.resultConverter.convertToReadResourceResult(result, request.uri(),
this.mimeType, this.contentType));
this.mimeType, this.contentType, this.meta));
}
}
catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
* handles URI template variables.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
public final class AsyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
implements BiFunction<McpTransportContext, ReadResourceRequest, Mono<ReadResourceResult>> {

private AsyncStatelessMcpResourceMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType);
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
this.validateMethod(this.method);
}

Expand Down Expand Up @@ -120,13 +121,13 @@ public Mono<ReadResourceResult> apply(McpTransportContext context, ReadResourceR
if (result instanceof Mono<?>) {
// If the result is already a Mono, use it
return ((Mono<?>) result).map(r -> this.resultConverter.convertToReadResourceResult(r,
request.uri(), this.mimeType, this.contentType));
request.uri(), this.mimeType, this.contentType, this.meta));
}
else {
// Otherwise, convert the result to a ReadResourceResult and wrap in a
// Mono
return Mono.just(this.resultConverter.convertToReadResourceResult(result, request.uri(),
this.mimeType, this.contentType));
this.mimeType, this.contentType, this.meta));
}
}
catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springaicommunity.mcp.method.resource.AbstractMcpResourceMethodCallback.ContentType;

Expand All @@ -21,6 +22,7 @@
* resource methods to a standardized {@link ReadResourceResult} format.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
public class DefaultMcpReadResourceResultConverter implements McpReadResourceResultConverter {

Expand All @@ -44,6 +46,23 @@ public class DefaultMcpReadResourceResultConverter implements McpReadResourceRes
@Override
public ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
ContentType contentType) {
return convertToReadResourceResult(result, requestUri, mimeType, contentType, null);
}

/**
* Converts the method's return value to a {@link ReadResourceResult}, propagating
* resource-level metadata to the content items.
* @param result The method's return value
* @param requestUri The original request URI
* @param mimeType The MIME type of the resource
* @param contentType The content type of the resource
* @param meta The resource-level metadata to propagate to content items
* @return A {@link ReadResourceResult} containing the appropriate resource contents
* @throws IllegalArgumentException if the return type is not supported
*/
@Override
public ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
ContentType contentType, Map<String, Object> meta) {
if (result == null) {
return new ReadResourceResult(List.of());
}
Expand All @@ -62,7 +81,7 @@ public ReadResourceResult convertToReadResourceResult(Object result, String requ
List<ResourceContents> contents;

if (result instanceof List<?>) {
contents = convertListResult((List<?>) result, requestUri, contentType, mimeType);
contents = convertListResult((List<?>) result, requestUri, contentType, mimeType, meta);
}
else if (result instanceof ResourceContents) {
// Single ResourceContents
Expand All @@ -71,7 +90,7 @@ else if (result instanceof ResourceContents) {
else if (result instanceof String) {
// Single String -> ResourceContents (TextResourceContents or
// BlobResourceContents)
contents = convertStringResult((String) result, requestUri, contentType, mimeType);
contents = convertStringResult((String) result, requestUri, contentType, mimeType, meta);
}
else {
throw new IllegalArgumentException("Unsupported return type: " + result.getClass().getName());
Expand All @@ -98,17 +117,18 @@ private boolean isTextMimeType(String mimeType) {
}

/**
* Converts a List result to a list of ResourceContents.
* Converts a List result to a list of ResourceContents with metadata.
* @param list The list result
* @param requestUri The original request URI
* @param contentType The content type (TEXT or BLOB)
* @param mimeType The MIME type
* @param meta The resource-level metadata to propagate to content items
* @return A list of ResourceContents
* @throws IllegalArgumentException if the list item type is not supported
*/
@SuppressWarnings("unchecked")
private List<ResourceContents> convertListResult(List<?> list, String requestUri, ContentType contentType,
String mimeType) {
String mimeType, Map<String, Object> meta) {
if (list.isEmpty()) {
return List.of();
}
Expand All @@ -127,12 +147,12 @@ else if (firstItem instanceof String) {

if (contentType == ContentType.TEXT) {
for (String text : stringList) {
result.add(new TextResourceContents(requestUri, mimeType, text));
result.add(new TextResourceContents(requestUri, mimeType, text, meta));
}
}
else { // BLOB
for (String blob : stringList) {
result.add(new BlobResourceContents(requestUri, mimeType, blob));
result.add(new BlobResourceContents(requestUri, mimeType, blob, meta));
}
}

Expand All @@ -145,20 +165,21 @@ else if (firstItem instanceof String) {
}

/**
* Converts a String result to a list of ResourceContents.
* Converts a String result to a list of ResourceContents with metadata.
* @param stringResult The string result
* @param requestUri The original request URI
* @param contentType The content type (TEXT or BLOB)
* @param mimeType The MIME type
* @param meta The resource-level metadata to propagate to content items
* @return A list containing a single ResourceContents
*/
private List<ResourceContents> convertStringResult(String stringResult, String requestUri, ContentType contentType,
String mimeType) {
String mimeType, Map<String, Object> meta) {
if (contentType == ContentType.TEXT) {
return List.of(new TextResourceContents(requestUri, mimeType, stringResult));
return List.of(new TextResourceContents(requestUri, mimeType, stringResult, meta));
}
else { // BLOB
return List.of(new BlobResourceContents(requestUri, mimeType, stringResult));
return List.of(new BlobResourceContents(requestUri, mimeType, stringResult, meta));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package org.springaicommunity.mcp.method.resource;

import java.util.Map;

import org.springaicommunity.mcp.method.resource.AbstractMcpResourceMethodCallback.ContentType;

import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
Expand All @@ -15,6 +17,7 @@
* methods to a standardized {@link ReadResourceResult} format.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
public interface McpReadResourceResultConverter {

Expand All @@ -33,4 +36,24 @@ public interface McpReadResourceResultConverter {
ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
ContentType contentType);

/**
* Converts the method's return value to a {@link ReadResourceResult}, propagating
* resource-level metadata to the content items.
* <p>
* This default method delegates to the original
* {@link #convertToReadResourceResult(Object, String, String, ContentType)} to ensure
* backwards compatibility with existing custom implementations.
* @param result The method's return value
* @param requestUri The original request URI
* @param mimeType The MIME type of the resource
* @param contentType The content type of the resource
* @param meta The resource-level metadata to propagate to content items
* @return A {@link ReadResourceResult} containing the appropriate resource contents
* @throws IllegalArgumentException if the return type is not supported
*/
default ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
ContentType contentType, Map<String, Object> meta) {
return convertToReadResourceResult(result, requestUri, mimeType, contentType);
}

}
Loading