diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/MetaUtils.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/MetaUtils.java
new file mode 100644
index 0000000..93f6640
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/MetaUtils.java
@@ -0,0 +1,87 @@
+package org.springaicommunity.mcp;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collections;
+import java.util.Map;
+
+import org.springaicommunity.mcp.context.MetaProvider;
+
+/**
+ * Utility methods for working with {@link MetaProvider} metadata.
+ *
+ *
+ * This class provides a single entry point {@link #getMeta(Class)} that instantiates the
+ * given provider type via a no-argument constructor and returns its metadata as an
+ * unmodifiable {@link Map}.
+ *
+ *
+ *
+ * Instantiation failures and missing no-arg constructors are reported as
+ * {@link IllegalArgumentException IllegalArgumentExceptions}. This class is stateless and
+ * not intended to be instantiated.
+ *
+ */
+public final class MetaUtils {
+
+ /** Not intended to be instantiated. */
+ private MetaUtils() {
+ }
+
+ /**
+ * Instantiate the supplied {@link MetaProvider} type using a no-argument constructor
+ * and return the metadata it supplies.
+ *
+ * The returned map is wrapped in {@link Collections#unmodifiableMap(Map)} to prevent
+ * external modification. If the provider returns {@code null}, this method also
+ * returns {@code null}.
+ * @param metaProviderClass the {@code MetaProvider} implementation class to
+ * instantiate; must provide a no-arg constructor
+ * @return an unmodifiable metadata map, or {@code null} if the provider returns
+ * {@code null}
+ * @throws IllegalArgumentException if a no-arg constructor is missing or the instance
+ * cannot be created
+ */
+ public static Map getMeta(Class extends MetaProvider> metaProviderClass) {
+
+ if (metaProviderClass == null) {
+ return null;
+ }
+
+ String className = metaProviderClass.getName();
+ MetaProvider metaProvider;
+ try {
+ // Prefer a public no-arg constructor; fall back to a declared no-arg if
+ // accessible
+ Constructor extends MetaProvider> constructor = getConstructor(metaProviderClass);
+ metaProvider = constructor.newInstance();
+ }
+ catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Required no-arg constructor not found in " + className, e);
+ }
+ catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
+ throw new IllegalArgumentException(className + " instantiation failed", e);
+ }
+
+ Map meta = metaProvider.getMeta();
+ return meta == null ? null : Collections.unmodifiableMap(meta);
+ }
+
+ /**
+ * Locate a no-argument constructor on the given class: prefer public, otherwise fall
+ * back to a declared no-arg constructor.
+ * @param metaProviderClass the class to inspect
+ * @return the resolved no-arg constructor
+ * @throws NoSuchMethodException if the class does not declare any no-arg constructor
+ */
+ private static Constructor extends MetaProvider> getConstructor(Class extends MetaProvider> metaProviderClass)
+ throws NoSuchMethodException {
+ try {
+ return metaProviderClass.getDeclaredConstructor();
+ }
+ catch (NoSuchMethodException ex) {
+ return metaProviderClass.getConstructor();
+ }
+ }
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/PromptAdapter.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/PromptAdapter.java
index 5d0c428..8c870d8 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/PromptAdapter.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/PromptAdapter.java
@@ -7,9 +7,11 @@
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Assert;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpArg;
import org.springaicommunity.mcp.annotation.McpPrompt;
@@ -29,7 +31,8 @@ private PromptAdapter() {
* @return The corresponding McpSchema.Prompt object
*/
public static McpSchema.Prompt asPrompt(McpPrompt mcpPrompt) {
- return new McpSchema.Prompt(mcpPrompt.name(), mcpPrompt.title(), mcpPrompt.description(), List.of());
+ Map meta = MetaUtils.getMeta(mcpPrompt.metaProvider());
+ return new McpSchema.Prompt(mcpPrompt.name(), mcpPrompt.title(), mcpPrompt.description(), List.of(), meta);
}
/**
@@ -41,7 +44,9 @@ public static McpSchema.Prompt asPrompt(McpPrompt mcpPrompt) {
*/
public static McpSchema.Prompt asPrompt(McpPrompt mcpPrompt, Method method) {
List arguments = extractPromptArguments(method);
- return new McpSchema.Prompt(getName(mcpPrompt, method), mcpPrompt.title(), mcpPrompt.description(), arguments);
+ Map meta = MetaUtils.getMeta(mcpPrompt.metaProvider());
+ return new McpSchema.Prompt(getName(mcpPrompt, method), mcpPrompt.title(), mcpPrompt.description(), arguments,
+ meta);
}
private static String getName(McpPrompt promptAnnotation, Method method) {
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java
index 5002e7f..65531c5 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpPrompt.java
@@ -4,6 +4,9 @@
package org.springaicommunity.mcp.annotation;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
+
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -35,4 +38,11 @@
*/
String description() default "";
+ /**
+ * Optional meta provider class that implements the MetaProvider interface. Used to
+ * provide additional metadata for the prompt. Defaults to {@link DefaultMetaProvider
+ * DefaultMetaProvider.class} if not specified.
+ */
+ Class extends MetaProvider> metaProvider() default DefaultMetaProvider.class;
+
}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java
index 40e3e0d..210ed4b 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java
@@ -11,6 +11,8 @@
import java.lang.annotation.Target;
import io.modelcontextprotocol.spec.McpSchema.Role;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
/**
* Marks a method as a MCP Resource.
@@ -56,6 +58,13 @@
*/
McpAnnotations annotations() default @McpAnnotations(audience = { Role.USER }, lastModified = "", priority = 0.5);
+ /**
+ * Optional meta provider class that supplies data for "_meta" field for this resource
+ * declaration. Defaults to {@link DefaultMetaProvider} implementation.
+ * @return the meta provider class to use for this resource
+ */
+ Class extends MetaProvider> metaProvider() default DefaultMetaProvider.class;
+
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface McpAnnotations {
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpTool.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpTool.java
index 8557623..dcf21ec 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpTool.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpTool.java
@@ -3,11 +3,15 @@
*/
package org.springaicommunity.mcp.annotation;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
+
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.util.Map;
/**
* @author Christian Tzolov
@@ -46,6 +50,12 @@
*/
String title() default "";
+ /**
+ * "_meta" field for the tool declaration. If not provided, no "_meta" appended to the
+ * tool specification.
+ */
+ Class extends MetaProvider> metaProvider() default DefaultMetaProvider.class;
+
/**
* Additional properties describing a Tool to clients.
*
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/DefaultMetaProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/DefaultMetaProvider.java
new file mode 100644
index 0000000..766a059
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/DefaultMetaProvider.java
@@ -0,0 +1,29 @@
+package org.springaicommunity.mcp.context;
+
+import java.util.Map;
+
+/**
+ * Default {@link MetaProvider} implementation that disables the "_meta" field in tool,
+ * prompt, resource declarations.
+ *
+ *
+ * This provider deliberately returns {@code null} from {@link #getMeta()} to signal that
+ * no "_meta" information is included.
+ *
+ *
+ *
+ * Use this when your tool, prompt, or resource does not need to expose any meta
+ * information or you want to keep responses minimal by default.
+ *
+ */
+public class DefaultMetaProvider implements MetaProvider {
+
+ /**
+ * Returns {@code null} to indicate that no "_meta" field should be included in.
+ */
+ @Override
+ public Map getMeta() {
+ return null;
+ }
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/MetaProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/MetaProvider.java
new file mode 100644
index 0000000..0d37863
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/MetaProvider.java
@@ -0,0 +1,20 @@
+package org.springaicommunity.mcp.context;
+
+import java.util.Map;
+
+/**
+ * Common interface for classes that provide metadata for the "_meta" field. This metadata
+ * is used in tool, prompt, and resource declarations.
+ */
+public interface MetaProvider {
+
+ /**
+ * Returns metadata key-value pairs that will be included in the "_meta" field. These
+ * metadata values provide additional context and information for tools, prompts, and
+ * resource declarations.
+ * @return A Map containing metadata key-value pairs, where keys are strings and
+ * values can be any object type.
+ */
+ Map getMeta();
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java
index 87c91b7..e5345a7 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java
@@ -16,12 +16,6 @@
package org.springaicommunity.mcp.provider.resource;
-import java.lang.reflect.Method;
-import java.util.List;
-import java.util.Objects;
-import java.util.function.BiFunction;
-import java.util.stream.Stream;
-
import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification;
import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceTemplateSpecification;
@@ -32,10 +26,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.method.resource.AsyncMcpResourceMethodCallback;
import reactor.core.publisher.Mono;
+import java.lang.reflect.Method;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
/**
* Provider for asynchronous MCP resource methods.
*
@@ -71,7 +73,7 @@ public List getResourceSpecifications() {
.map(resourceObject -> Stream.of(doGetClassMethods(resourceObject))
.filter(method -> method.isAnnotationPresent(McpResource.class))
.filter(McpPredicates.filterNonReactiveReturnTypeMethod())
- .sorted((m1, m2) -> m1.getName().compareTo(m2.getName()))
+ .sorted(Comparator.comparing(Method::getName))
.map(mcpResourceMethod -> {
var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod);
@@ -85,12 +87,14 @@ public List getResourceSpecifications() {
var name = getName(mcpResourceMethod, resourceAnnotation);
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
+ var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
var mcpResource = McpSchema.Resource.builder()
.uri(uri)
.name(name)
.description(description)
.mimeType(mimeType)
+ .meta(meta)
.build();
BiFunction> methodCallback = AsyncMcpResourceMethodCallback
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java
index 4674236..e2c2fc6 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java
@@ -32,6 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.method.resource.AsyncStatelessMcpResourceMethodCallback;
import reactor.core.publisher.Mono;
@@ -86,12 +87,14 @@ public List getResourceSpecifications() {
var name = getName(mcpResourceMethod, resourceAnnotation);
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
+ var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
var mcpResource = McpSchema.Resource.builder()
.uri(uri)
.name(name)
.description(description)
.mimeType(mimeType)
+ .meta(meta)
.build();
BiFunction> methodCallback = AsyncStatelessMcpResourceMethodCallback
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java
index ad9fd15..27a8409 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java
@@ -26,6 +26,7 @@
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Assert;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.method.resource.SyncMcpResourceMethodCallback;
@@ -59,12 +60,14 @@ public List getResourceSpecifications() {
var name = getName(mcpResourceMethod, resourceAnnotation);
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
+ var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
var mcpResource = McpSchema.Resource.builder()
.uri(uri)
.name(name)
.description(description)
.mimeType(mimeType)
+ .meta(meta)
.build();
var methodCallback = SyncMcpResourceMethodCallback.builder()
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java
index 6905cd0..d0ca8f4 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java
@@ -32,6 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.method.resource.SyncStatelessMcpResourceMethodCallback;
@@ -85,12 +86,14 @@ public List getResourceSpecifications() {
var name = getName(mcpResourceMethod, resourceAnnotation);
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
+ var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
var mcpResource = McpSchema.Resource.builder()
.uri(uri)
.name(name)
.description(description)
.mimeType(mimeType)
+ .meta(meta)
.build();
BiFunction methodCallback = SyncStatelessMcpResourceMethodCallback
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java
index 0be1e8d..8475936 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java
@@ -16,6 +16,8 @@
package org.springaicommunity.mcp.provider.tool;
+import java.lang.reflect.Method;
+import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Stream;
@@ -29,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.method.tool.AsyncMcpToolMethodCallback;
import org.springaicommunity.mcp.method.tool.ReactiveUtils;
@@ -64,7 +67,7 @@ public List getToolSpecifications() {
.map(toolObject -> Stream.of(this.doGetClassMethods(toolObject))
.filter(method -> method.isAnnotationPresent(McpTool.class))
.filter(McpPredicates.filterNonReactiveReturnTypeMethod())
- .sorted((m1, m2) -> m1.getName().compareTo(m2.getName()))
+ .sorted(Comparator.comparing(Method::getName))
.map(mcpToolMethod -> {
var toolJavaAnnotation = this.doGetMcpToolAnnotation(mcpToolMethod);
@@ -72,14 +75,17 @@ public List getToolSpecifications() {
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();
- String toolDescrption = toolJavaAnnotation.description();
+ String toolDescription = toolJavaAnnotation.description();
String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
+ var meta = MetaUtils.getMeta(toolJavaAnnotation.metaProvider());
+
var toolBuilder = McpSchema.Tool.builder()
.name(toolName)
- .description(toolDescrption)
- .inputSchema(this.getJsonMapper(), inputSchema);
+ .description(toolDescription)
+ .inputSchema(this.getJsonMapper(), inputSchema)
+ .meta(meta);
var title = toolJavaAnnotation.title();
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java
index bfc7049..37ef14d 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java
@@ -16,6 +16,8 @@
package org.springaicommunity.mcp.provider.tool;
+import java.lang.reflect.Method;
+import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Stream;
@@ -29,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.method.tool.AsyncStatelessMcpToolMethodCallback;
import org.springaicommunity.mcp.method.tool.ReactiveUtils;
@@ -69,7 +72,7 @@ public List getToolSpecifications() {
.filter(method -> method.isAnnotationPresent(McpTool.class))
.filter(McpPredicates.filterNonReactiveReturnTypeMethod())
.filter(McpPredicates.filterMethodWithBidirectionalParameters())
- .sorted((m1, m2) -> m1.getName().compareTo(m2.getName()))
+ .sorted(Comparator.comparing(Method::getName))
.map(mcpToolMethod -> {
var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod);
@@ -77,14 +80,17 @@ public List getToolSpecifications() {
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();
- String toolDescrption = toolJavaAnnotation.description();
+ String toolDescription = toolJavaAnnotation.description();
String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
+ var meta = MetaUtils.getMeta(toolJavaAnnotation.metaProvider());
+
var toolBuilder = McpSchema.Tool.builder()
.name(toolName)
- .description(toolDescrption)
- .inputSchema(this.getJsonMapper(), inputSchema);
+ .description(toolDescription)
+ .inputSchema(this.getJsonMapper(), inputSchema)
+ .meta(meta);
var title = toolJavaAnnotation.title();
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java
index e209b73..8b2efd9 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java
@@ -16,6 +16,8 @@
package org.springaicommunity.mcp.provider.tool;
+import java.lang.reflect.Method;
+import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Stream;
@@ -29,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.method.tool.ReturnMode;
import org.springaicommunity.mcp.method.tool.SyncMcpToolMethodCallback;
@@ -62,7 +65,7 @@ public List getToolSpecifications() {
.map(toolObject -> Stream.of(this.doGetClassMethods(toolObject))
.filter(method -> method.isAnnotationPresent(McpTool.class))
.filter(McpPredicates.filterReactiveReturnTypeMethod())
- .sorted((m1, m2) -> m1.getName().compareTo(m2.getName()))
+ .sorted(Comparator.comparing(Method::getName))
.map(mcpToolMethod -> {
McpTool toolJavaAnnotation = this.doGetMcpToolAnnotation(mcpToolMethod);
@@ -74,10 +77,13 @@ public List getToolSpecifications() {
String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
- McpSchema.Tool.Builder toolBuilder = McpSchema.Tool.builder()
+ var meta = MetaUtils.getMeta(toolJavaAnnotation.metaProvider());
+
+ var toolBuilder = McpSchema.Tool.builder()
.name(toolName)
.description(toolDescription)
- .inputSchema(this.getJsonMapper(), inputSchema);
+ .inputSchema(this.getJsonMapper(), inputSchema)
+ .meta(meta);
var title = toolJavaAnnotation.title();
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java
index c1ce5f6..e7fb5f8 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java
@@ -16,6 +16,8 @@
package org.springaicommunity.mcp.provider.tool;
+import java.lang.reflect.Method;
+import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Stream;
@@ -29,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.McpPredicates;
+import org.springaicommunity.mcp.MetaUtils;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.method.tool.ReturnMode;
import org.springaicommunity.mcp.method.tool.SyncStatelessMcpToolMethodCallback;
@@ -66,7 +69,7 @@ public List getToolSpecifications() {
.filter(method -> method.isAnnotationPresent(McpTool.class))
.filter(McpPredicates.filterReactiveReturnTypeMethod())
.filter(McpPredicates.filterMethodWithBidirectionalParameters())
- .sorted((m1, m2) -> m1.getName().compareTo(m2.getName()))
+ .sorted(Comparator.comparing(Method::getName))
.map(mcpToolMethod -> {
var toolJavaAnnotation = this.doGetMcpToolAnnotation(mcpToolMethod);
@@ -74,14 +77,17 @@ public List getToolSpecifications() {
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();
- String toolDescrption = toolJavaAnnotation.description();
+ String toolDescription = toolJavaAnnotation.description();
String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);
+ var meta = MetaUtils.getMeta(toolJavaAnnotation.metaProvider());
+
var toolBuilder = McpSchema.Tool.builder()
.name(toolName)
- .description(toolDescrption)
- .inputSchema(this.getJsonMapper(), inputSchema);
+ .description(toolDescription)
+ .inputSchema(this.getJsonMapper(), inputSchema)
+ .meta(meta);
var title = toolJavaAnnotation.title();
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/MetaUtilsTest.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/MetaUtilsTest.java
new file mode 100644
index 0000000..752fa3b
--- /dev/null
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/MetaUtilsTest.java
@@ -0,0 +1,100 @@
+package org.springaicommunity.mcp;
+
+import org.junit.jupiter.api.Test;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+class MetaUtilsTest {
+
+ @Test
+ void testGetMetaNonNull() {
+
+ Map actual = MetaUtils.getMeta(MetaProviderWithDefaultConstructor.class);
+
+ assertThat(actual).containsExactlyInAnyOrderEntriesOf(new MetaProviderWithDefaultConstructor().getMeta());
+ }
+
+ @Test
+ void testGetMetaWithPublicConstructor() {
+
+ Map actual = MetaUtils.getMeta(MetaProviderWithAvailableConstructor.class);
+
+ assertThat(actual).containsExactlyInAnyOrderEntriesOf(new MetaProviderWithAvailableConstructor().getMeta());
+ }
+
+ @Test
+ void testGetMetaWithUnavailableConstructor() {
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> MetaUtils.getMeta(MetaProviderWithUnavailableConstructor.class))
+ .withMessage(
+ "org.springaicommunity.mcp.MetaUtilsTest$MetaProviderWithUnavailableConstructor instantiation failed");
+ }
+
+ @Test
+ void testGetMetaWithConstructorWithWrongSignature() {
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> MetaUtils.getMeta(MetaProviderWithConstructorWithWrongSignature.class))
+ .withMessage(
+ "Required no-arg constructor not found in org.springaicommunity.mcp.MetaUtilsTest$MetaProviderWithConstructorWithWrongSignature");
+ }
+
+ @Test
+ void testGetMetaNull() {
+
+ Map actual = MetaUtils.getMeta(DefaultMetaProvider.class);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ void testMetaProviderClassIsNullReturnsNull() {
+
+ Map actual = MetaUtils.getMeta(null);
+
+ assertThat(actual).isNull();
+ }
+
+ static class MetaProviderWithDefaultConstructor implements MetaProvider {
+
+ @Override
+ public Map getMeta() {
+ return Map.of("a", "1", "b", "2");
+ }
+
+ }
+
+ @SuppressWarnings("unused")
+ static class MetaProviderWithAvailableConstructor extends MetaProviderWithDefaultConstructor {
+
+ public MetaProviderWithAvailableConstructor() {
+ // Nothing to do here
+ }
+
+ }
+
+ @SuppressWarnings("unused")
+ static class MetaProviderWithUnavailableConstructor extends MetaProviderWithDefaultConstructor {
+
+ private MetaProviderWithUnavailableConstructor() {
+ // Nothing to do here
+ }
+
+ }
+
+ @SuppressWarnings("unused")
+ static class MetaProviderWithConstructorWithWrongSignature extends MetaProviderWithDefaultConstructor {
+
+ private MetaProviderWithConstructorWithWrongSignature(int invalid) {
+ // Nothing to do here
+ }
+
+ }
+
+}
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/context/DefaultMetaProviderTest.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/context/DefaultMetaProviderTest.java
new file mode 100644
index 0000000..77a9866
--- /dev/null
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/context/DefaultMetaProviderTest.java
@@ -0,0 +1,22 @@
+package org.springaicommunity.mcp.context;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+class DefaultMetaProviderTest {
+
+ @Test
+ void testGetMetaReturningNull() {
+
+ DefaultMetaProvider provider = new DefaultMetaProvider();
+
+ Map actual = provider.getMeta();
+
+ assertThat(actual).isNull();
+ }
+
+}
\ No newline at end of file
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java
index 7234cc1..33142b6 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java
@@ -26,8 +26,10 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
import org.springaicommunity.mcp.annotation.McpResource;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
import org.springaicommunity.mcp.context.McpAsyncRequestContext;
import org.springaicommunity.mcp.context.McpSyncRequestContext;
+import org.springaicommunity.mcp.context.MetaProvider;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -337,6 +339,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
}
@@ -676,6 +683,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
assertThatThrownBy(() -> AsyncMcpResourceMethodCallback.builder()
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java
index be03bea..cbf79ff 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java
@@ -26,6 +26,8 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
import org.springaicommunity.mcp.annotation.McpResource;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -266,6 +268,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
}
@@ -611,6 +618,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
assertThatThrownBy(() -> AsyncStatelessMcpResourceMethodCallback.builder()
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java
index 5cb4ad4..4692e29 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java
@@ -12,6 +12,8 @@
import io.modelcontextprotocol.spec.McpSchema.Role;
import org.springaicommunity.mcp.adapter.ResourceAdapter;
import org.springaicommunity.mcp.annotation.McpResource;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
/**
* Simple test to verify that McpResourceMethodCallback requires a non-empty URI in the
@@ -90,6 +92,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
}
@@ -150,6 +157,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
}
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java
index 92da973..e1176b0 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java
@@ -23,9 +23,11 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
import org.springaicommunity.mcp.annotation.McpResource;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
import org.springaicommunity.mcp.context.McpAsyncRequestContext;
import org.springaicommunity.mcp.context.McpSyncRequestContext;
+import org.springaicommunity.mcp.context.MetaProvider;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
@@ -315,6 +317,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
}
@@ -615,6 +622,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
assertThatThrownBy(() -> SyncMcpResourceMethodCallback.builder()
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java
index eee5647..c551695 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java
@@ -24,6 +24,8 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
import org.springaicommunity.mcp.annotation.McpResource;
+import org.springaicommunity.mcp.context.DefaultMetaProvider;
+import org.springaicommunity.mcp.context.MetaProvider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -248,6 +250,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
}
@@ -660,6 +667,11 @@ public double priority() {
}
};
}
+
+ @Override
+ public Class extends MetaProvider> metaProvider() {
+ return DefaultMetaProvider.class;
+ }
};
assertThatThrownBy(() -> SyncStatelessMcpResourceMethodCallback.builder()