Skip to content
Open
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
@@ -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.
*
* <p>
* 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}.
* </p>
*
* <p>
* Instantiation failures and missing no-arg constructors are reported as
* {@link IllegalArgumentException IllegalArgumentExceptions}. This class is stateless and
* not intended to be instantiated.
* </p>
*/
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.
* <p>
* 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<String, Object> 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<String, Object> 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();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<String, Object> meta = MetaUtils.getMeta(mcpPrompt.metaProvider());
return new McpSchema.Prompt(mcpPrompt.name(), mcpPrompt.title(), mcpPrompt.description(), List.of(), meta);
}

/**
Expand All @@ -41,7 +44,9 @@ public static McpSchema.Prompt asPrompt(McpPrompt mcpPrompt) {
*/
public static McpSchema.Prompt asPrompt(McpPrompt mcpPrompt, Method method) {
List<McpSchema.PromptArgument> arguments = extractPromptArguments(method);
return new McpSchema.Prompt(getName(mcpPrompt, method), mcpPrompt.title(), mcpPrompt.description(), arguments);
Map<String, Object> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* This provider deliberately returns {@code null} from {@link #getMeta()} to signal that
* no "_meta" information is included.
* </p>
*
* <p>
* 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.
* </p>
*/
public class DefaultMetaProvider implements MetaProvider {

/**
* Returns {@code null} to indicate that no "_meta" field should be included in.
*/
@Override
public Map<String, Object> getMeta() {
return null;
}

}
Original file line number Diff line number Diff line change
@@ -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<String, Object> getMeta();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -71,7 +73,7 @@ public List<AsyncResourceSpecification> 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);
Expand All @@ -85,12 +87,14 @@ public List<AsyncResourceSpecification> 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<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> methodCallback = AsyncMcpResourceMethodCallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -86,12 +87,14 @@ public List<AsyncResourceSpecification> 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<McpTransportContext, ReadResourceRequest, Mono<ReadResourceResult>> methodCallback = AsyncStatelessMcpResourceMethodCallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -59,12 +60,14 @@ public List<SyncResourceSpecification> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -85,12 +86,14 @@ public List<SyncResourceSpecification> 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<McpTransportContext, ReadResourceRequest, ReadResourceResult> methodCallback = SyncStatelessMcpResourceMethodCallback
Expand Down
Loading