Skip to content

Commit 3dbf7c4

Browse files
committed
feat: add _meta support to @mcptool and @McpResource annotations
Add `meta` attribute to both annotations, enabling MCP Apps UI metadata (e.g. _meta.ui.resourceUri, _meta.ui.csp) to be declared directly on annotated methods. Meta propagates to tool/resource declarations and to resource content in ReadResourceResult. Signed-off-by: Alexandros Pappas <apappascs@gmail.com>
1 parent a6c2d38 commit 3dbf7c4

27 files changed

Lines changed: 515 additions & 30 deletions

mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/ResourceAdapter.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
package org.springaicommunity.mcp.adapter;
55

66
import java.util.List;
7+
import java.util.Map;
78

89
import io.modelcontextprotocol.spec.McpSchema;
10+
import io.modelcontextprotocol.util.Utils;
911
import org.springaicommunity.mcp.annotation.McpResource;
12+
import org.springaicommunity.mcp.method.tool.utils.JsonParser;
1013

1114
/**
1215
* @author Christian Tzolov
16+
* @author Alexandros Pappas
1317
*/
1418
public class ResourceAdapter {
1519

@@ -27,7 +31,8 @@ public static McpSchema.Resource asResource(McpResource mcpResourceAnnotation) {
2731
.name(name)
2832
.title(mcpResourceAnnotation.title())
2933
.description(mcpResourceAnnotation.description())
30-
.mimeType(mcpResourceAnnotation.mimeType());
34+
.mimeType(mcpResourceAnnotation.mimeType())
35+
.meta(parseMeta(mcpResourceAnnotation.meta()));
3136

3237
// Only set annotations if not default value is provided
3338
// This is a workaround since Java annotations do not support null default values
@@ -48,8 +53,21 @@ public static McpSchema.ResourceTemplate asResourceTemplate(McpResource mcpResou
4853
if (name == null || name.isEmpty()) {
4954
name = "resource"; // Default name when not specified
5055
}
51-
return new McpSchema.ResourceTemplate(mcpResource.uri(), name, mcpResource.description(),
52-
mcpResource.mimeType(), null);
56+
return McpSchema.ResourceTemplate.builder()
57+
.uriTemplate(mcpResource.uri())
58+
.name(name)
59+
.description(mcpResource.description())
60+
.mimeType(mcpResource.mimeType())
61+
.meta(parseMeta(mcpResource.meta()))
62+
.build();
63+
}
64+
65+
@SuppressWarnings("unchecked")
66+
private static Map<String, Object> parseMeta(String metaJson) {
67+
if (!Utils.hasText(metaJson)) {
68+
return null;
69+
}
70+
return JsonParser.fromJson(metaJson, Map.class);
5371
}
5472

5573
}

mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* Marks a method as a MCP Resource.
1717
*
1818
* @author Christian Tzolov
19+
* @author Alexandros Pappas
1920
*/
2021
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
2122
@Retention(RetentionPolicy.RUNTIME)
@@ -50,6 +51,12 @@
5051
*/
5152
String mimeType() default "text/plain";
5253

54+
/**
55+
* Optional JSON string representing the _meta field for this resource. The value is
56+
* parsed as a JSON object and passed to the Resource builder's meta method.
57+
*/
58+
String meta() default "";
59+
5360
/**
5461
* Optional annotations for the client. Note: The default annotations value is
5562
* ignored.

mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpTool.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
/**
1313
* @author Christian Tzolov
14+
* @author Alexandros Pappas
1415
*/
1516
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
1617
@Retention(RetentionPolicy.RUNTIME)
@@ -46,6 +47,12 @@
4647
*/
4748
String title() default "";
4849

50+
/**
51+
* Optional JSON string representing the _meta field for this tool. The value is
52+
* parsed as a JSON object and passed to the Tool builder's meta method.
53+
*/
54+
String meta() default "";
55+
4956
/**
5057
* Additional properties describing a Tool to clients.
5158
*

mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AbstractMcpResourceMethodCallback.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* and other common operations.
3636
*
3737
* @author Christian Tzolov
38+
* @author Alexandros Pappas
3839
*/
3940
public abstract class AbstractMcpResourceMethodCallback {
4041

@@ -75,6 +76,8 @@ public enum ContentType {
7576

7677
protected final ContentType contentType;
7778

79+
protected final Map<String, Object> meta;
80+
7881
/**
7982
* Constructor for AbstractMcpResourceMethodCallback.
8083
* @param method The method to create a callback for
@@ -86,10 +89,11 @@ public enum ContentType {
8689
* @param resultConverter The result converter
8790
* @param uriTemplateMangerFactory The URI template manager factory
8891
* @param contentType The content type
92+
* @param meta The resource metadata to propagate to content-level _meta
8993
*/
9094
protected AbstractMcpResourceMethodCallback(Method method, Object bean, String uri, String name, String description,
9195
String mimeType, McpReadResourceResultConverter resultConverter,
92-
McpUriTemplateManagerFactory uriTemplateMangerFactory, ContentType contentType) {
96+
McpUriTemplateManagerFactory uriTemplateMangerFactory, ContentType contentType, Map<String, Object> meta) {
9397

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

111115
this.contentType = contentType;
116+
this.meta = meta;
112117
}
113118

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

568573
protected String uri; // Resource URI
569574

575+
protected Map<String, Object> meta; // Resource metadata
576+
570577
/**
571578
* Set the method to create a callback for.
572579
* @param method The method to create a callback for
@@ -609,6 +616,7 @@ public T resource(McpSchema.Resource resource) {
609616
this.name = resource.name();
610617
this.description = resource.description();
611618
this.mimeType = resource.mimeType();
619+
this.meta = resource.meta();
612620
return (T) this;
613621
}
614622

@@ -622,6 +630,7 @@ public T resource(McpSchema.ResourceTemplate resourceTemplate) {
622630
this.name = resourceTemplate.name();
623631
this.description = resourceTemplate.description();
624632
this.mimeType = resourceTemplate.mimeType();
633+
this.meta = resourceTemplate.meta();
625634
return (T) this;
626635
}
627636

mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallback.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@
3030
* variables.
3131
*
3232
* @author Christian Tzolov
33+
* @author Alexandros Pappas
3334
*/
3435
public final class AsyncMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
3536
implements BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> {
3637

3738
private AsyncMcpResourceMethodCallback(Builder builder) {
3839
super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
39-
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType);
40+
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
4041
this.validateMethod(this.method);
4142
}
4243

@@ -126,13 +127,13 @@ public Mono<ReadResourceResult> apply(McpAsyncServerExchange exchange, ReadResou
126127
if (result instanceof Mono<?>) {
127128
// If the result is already a Mono, use it
128129
return ((Mono<?>) result).map(r -> this.resultConverter.convertToReadResourceResult(r,
129-
request.uri(), this.mimeType, this.contentType));
130+
request.uri(), this.mimeType, this.contentType, this.meta));
130131
}
131132
else {
132133
// Otherwise, convert the result to a ReadResourceResult and wrap in a
133134
// Mono
134135
return Mono.just(this.resultConverter.convertToReadResourceResult(result, request.uri(),
135-
this.mimeType, this.contentType));
136+
this.mimeType, this.contentType, this.meta));
136137
}
137138
}
138139
catch (Exception e) {

mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallback.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@
3030
* handles URI template variables.
3131
*
3232
* @author Christian Tzolov
33+
* @author Alexandros Pappas
3334
*/
3435
public final class AsyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
3536
implements BiFunction<McpTransportContext, ReadResourceRequest, Mono<ReadResourceResult>> {
3637

3738
private AsyncStatelessMcpResourceMethodCallback(Builder builder) {
3839
super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
39-
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType);
40+
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
4041
this.validateMethod(this.method);
4142
}
4243

@@ -120,13 +121,13 @@ public Mono<ReadResourceResult> apply(McpTransportContext context, ReadResourceR
120121
if (result instanceof Mono<?>) {
121122
// If the result is already a Mono, use it
122123
return ((Mono<?>) result).map(r -> this.resultConverter.convertToReadResourceResult(r,
123-
request.uri(), this.mimeType, this.contentType));
124+
request.uri(), this.mimeType, this.contentType, this.meta));
124125
}
125126
else {
126127
// Otherwise, convert the result to a ReadResourceResult and wrap in a
127128
// Mono
128129
return Mono.just(this.resultConverter.convertToReadResourceResult(result, request.uri(),
129-
this.mimeType, this.contentType));
130+
this.mimeType, this.contentType, this.meta));
130131
}
131132
}
132133
catch (Exception e) {

mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/DefaultMcpReadResourceResultConverter.java

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.util.ArrayList;
88
import java.util.List;
9+
import java.util.Map;
910

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

@@ -21,6 +22,7 @@
2122
* resource methods to a standardized {@link ReadResourceResult} format.
2223
*
2324
* @author Christian Tzolov
25+
* @author Alexandros Pappas
2426
*/
2527
public class DefaultMcpReadResourceResultConverter implements McpReadResourceResultConverter {
2628

@@ -44,6 +46,23 @@ public class DefaultMcpReadResourceResultConverter implements McpReadResourceRes
4446
@Override
4547
public ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
4648
ContentType contentType) {
49+
return convertToReadResourceResult(result, requestUri, mimeType, contentType, null);
50+
}
51+
52+
/**
53+
* Converts the method's return value to a {@link ReadResourceResult}, propagating
54+
* resource-level metadata to the content items.
55+
* @param result The method's return value
56+
* @param requestUri The original request URI
57+
* @param mimeType The MIME type of the resource
58+
* @param contentType The content type of the resource
59+
* @param meta The resource-level metadata to propagate to content items
60+
* @return A {@link ReadResourceResult} containing the appropriate resource contents
61+
* @throws IllegalArgumentException if the return type is not supported
62+
*/
63+
@Override
64+
public ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
65+
ContentType contentType, Map<String, Object> meta) {
4766
if (result == null) {
4867
return new ReadResourceResult(List.of());
4968
}
@@ -62,7 +81,7 @@ public ReadResourceResult convertToReadResourceResult(Object result, String requ
6281
List<ResourceContents> contents;
6382

6483
if (result instanceof List<?>) {
65-
contents = convertListResult((List<?>) result, requestUri, contentType, mimeType);
84+
contents = convertListResult((List<?>) result, requestUri, contentType, mimeType, meta);
6685
}
6786
else if (result instanceof ResourceContents) {
6887
// Single ResourceContents
@@ -71,7 +90,7 @@ else if (result instanceof ResourceContents) {
7190
else if (result instanceof String) {
7291
// Single String -> ResourceContents (TextResourceContents or
7392
// BlobResourceContents)
74-
contents = convertStringResult((String) result, requestUri, contentType, mimeType);
93+
contents = convertStringResult((String) result, requestUri, contentType, mimeType, meta);
7594
}
7695
else {
7796
throw new IllegalArgumentException("Unsupported return type: " + result.getClass().getName());
@@ -98,17 +117,18 @@ private boolean isTextMimeType(String mimeType) {
98117
}
99118

100119
/**
101-
* Converts a List result to a list of ResourceContents.
120+
* Converts a List result to a list of ResourceContents with metadata.
102121
* @param list The list result
103122
* @param requestUri The original request URI
104123
* @param contentType The content type (TEXT or BLOB)
105124
* @param mimeType The MIME type
125+
* @param meta The resource-level metadata to propagate to content items
106126
* @return A list of ResourceContents
107127
* @throws IllegalArgumentException if the list item type is not supported
108128
*/
109129
@SuppressWarnings("unchecked")
110130
private List<ResourceContents> convertListResult(List<?> list, String requestUri, ContentType contentType,
111-
String mimeType) {
131+
String mimeType, Map<String, Object> meta) {
112132
if (list.isEmpty()) {
113133
return List.of();
114134
}
@@ -127,12 +147,12 @@ else if (firstItem instanceof String) {
127147

128148
if (contentType == ContentType.TEXT) {
129149
for (String text : stringList) {
130-
result.add(new TextResourceContents(requestUri, mimeType, text));
150+
result.add(new TextResourceContents(requestUri, mimeType, text, meta));
131151
}
132152
}
133153
else { // BLOB
134154
for (String blob : stringList) {
135-
result.add(new BlobResourceContents(requestUri, mimeType, blob));
155+
result.add(new BlobResourceContents(requestUri, mimeType, blob, meta));
136156
}
137157
}
138158

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

147167
/**
148-
* Converts a String result to a list of ResourceContents.
168+
* Converts a String result to a list of ResourceContents with metadata.
149169
* @param stringResult The string result
150170
* @param requestUri The original request URI
151171
* @param contentType The content type (TEXT or BLOB)
152172
* @param mimeType The MIME type
173+
* @param meta The resource-level metadata to propagate to content items
153174
* @return A list containing a single ResourceContents
154175
*/
155176
private List<ResourceContents> convertStringResult(String stringResult, String requestUri, ContentType contentType,
156-
String mimeType) {
177+
String mimeType, Map<String, Object> meta) {
157178
if (contentType == ContentType.TEXT) {
158-
return List.of(new TextResourceContents(requestUri, mimeType, stringResult));
179+
return List.of(new TextResourceContents(requestUri, mimeType, stringResult, meta));
159180
}
160181
else { // BLOB
161-
return List.of(new BlobResourceContents(requestUri, mimeType, stringResult));
182+
return List.of(new BlobResourceContents(requestUri, mimeType, stringResult, meta));
162183
}
163184
}
164185

mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/McpReadResourceResultConverter.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package org.springaicommunity.mcp.method.resource;
66

7+
import java.util.Map;
8+
79
import org.springaicommunity.mcp.method.resource.AbstractMcpResourceMethodCallback.ContentType;
810

911
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
@@ -15,6 +17,7 @@
1517
* methods to a standardized {@link ReadResourceResult} format.
1618
*
1719
* @author Christian Tzolov
20+
* @author Alexandros Pappas
1821
*/
1922
public interface McpReadResourceResultConverter {
2023

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

39+
/**
40+
* Converts the method's return value to a {@link ReadResourceResult}, propagating
41+
* resource-level metadata to the content items.
42+
* <p>
43+
* This default method delegates to the original
44+
* {@link #convertToReadResourceResult(Object, String, String, ContentType)} to ensure
45+
* backwards compatibility with existing custom implementations.
46+
* @param result The method's return value
47+
* @param requestUri The original request URI
48+
* @param mimeType The MIME type of the resource
49+
* @param contentType The content type of the resource
50+
* @param meta The resource-level metadata to propagate to content items
51+
* @return A {@link ReadResourceResult} containing the appropriate resource contents
52+
* @throws IllegalArgumentException if the return type is not supported
53+
*/
54+
default ReadResourceResult convertToReadResourceResult(Object result, String requestUri, String mimeType,
55+
ContentType contentType, Map<String, Object> meta) {
56+
return convertToReadResourceResult(result, requestUri, mimeType, contentType);
57+
}
58+
3659
}

0 commit comments

Comments
 (0)