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
@@ -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,12 +4,17 @@
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.MetaUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.method.tool.utils.JsonParser;

/**
* @author Christian Tzolov
* @author Alexandros Pappas
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since here are multiple authors I would suggest to remove them from JavaDoc and keep them in the git log. Here and in 26 other changed files.

Also, it would be nice to write a couple words explaining the class purpose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've updated the javadocs to add the missing hames: #105

*/
public class ResourceAdapter {

Expand All @@ -21,13 +26,15 @@ public static McpSchema.Resource asResource(McpResource mcpResourceAnnotation) {
if (name == null || name.isEmpty()) {
name = "resource"; // Default name when not specified
}
var meta = MetaUtils.getMeta(mcpResourceAnnotation.metaProvider());

var resourceBuilder = McpSchema.Resource.builder()
.uri(mcpResourceAnnotation.uri())
.name(name)
.title(mcpResourceAnnotation.title())
.description(mcpResourceAnnotation.description())
.mimeType(mcpResourceAnnotation.mimeType());
.mimeType(mcpResourceAnnotation.mimeType())
.meta(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 +55,23 @@ 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);
var meta = MetaUtils.getMeta(mcpResource.metaProvider());

return McpSchema.ResourceTemplate.builder()
.uriTemplate(mcpResource.uri())
.name(name)
.description(mcpResource.description())
.mimeType(mcpResource.mimeType())
.meta(meta)
.build();
}

@SuppressWarnings("unchecked")
private static Map<String, Object> parseMeta(String metaJson) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this method in use? Also in AsyncStatelessMcpResourceProvider, SyncMcpResourceProvider, SyncStatelessMcpResourceProvider, AbstractMcpToolProvider.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it doesn't and was removed in #105

if (!Utils.hasText(metaJson)) {
return null;
}
return JsonParser.fromJson(metaJson, Map.class);
}

}
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,11 +11,14 @@
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.
*
* @author Christian Tzolov
* @author Alexandros Pappas
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
Expand Down Expand Up @@ -56,6 +59,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,14 +3,19 @@
*/
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
* @author Alexandros Pappas
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
Expand Down Expand Up @@ -46,6 +51,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 @@ -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
Loading