Skip to content
Merged
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 @@ -9,8 +9,6 @@
import java.util.function.Consumer;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
Expand Down Expand Up @@ -79,7 +77,8 @@ public <T> Mono<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec> spec
DefaultElicitationSpec elicitationSpec = new DefaultElicitationSpec();
spec.accept(elicitationSpec);
return this.elicitationInternal(elicitationSpec.message, type.getType(), elicitationSpec.meta)
.map(er -> new StructuredElicitResult<T>(er.action(), convertMapToType(er.content(), type), er.meta()));
.map(er -> new StructuredElicitResult<T>(er.action(), JsonParser.convertMapToType(er.content(), type),
er.meta()));
}

@Override
Expand All @@ -89,21 +88,24 @@ public <T> Mono<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec> spec
DefaultElicitationSpec elicitationSpec = new DefaultElicitationSpec();
spec.accept(elicitationSpec);
return this.elicitationInternal(elicitationSpec.message, type, elicitationSpec.meta)
.map(er -> new StructuredElicitResult<T>(er.action(), convertMapToType(er.content(), type), er.meta()));
.map(er -> new StructuredElicitResult<T>(er.action(), JsonParser.convertMapToType(er.content(), type),
er.meta()));
}

@Override
public <T> Mono<StructuredElicitResult<T>> elicit(TypeReference<T> type) {
Assert.notNull(type, "Elicitation response type must not be null");
return this.elicitationInternal("Please provide the required information.", type.getType(), null)
.map(er -> new StructuredElicitResult<T>(er.action(), convertMapToType(er.content(), type), er.meta()));
.map(er -> new StructuredElicitResult<T>(er.action(), JsonParser.convertMapToType(er.content(), type),
er.meta()));
}

@Override
public <T> Mono<StructuredElicitResult<T>> elicit(Class<T> type) {
Assert.notNull(type, "Elicitation response type must not be null");
return this.elicitationInternal("Please provide the required information.", type, null)
.map(er -> new StructuredElicitResult<T>(er.action(), convertMapToType(er.content(), type), er.meta()));
.map(er -> new StructuredElicitResult<T>(er.action(), JsonParser.convertMapToType(er.content(), type),
er.meta()));
}

@Override
Expand All @@ -124,6 +126,9 @@ public Mono<ElicitResult> elicitationInternal(String message, Type type, Map<Str
Assert.hasText(message, "Elicitation message must not be empty");
Assert.notNull(type, "Elicitation response type must not be null");

// TODO add validation for the Elicitation Schema
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#supported-schema-types

Map<String, Object> schema = typeSchemaCache.computeIfAbsent(type, t -> this.generateElicitSchema(t));

return this.elicit(ElicitRequest.builder().message(message).requestedSchema(schema).meta(meta).build());
Expand All @@ -136,18 +141,6 @@ private Map<String, Object> generateElicitSchema(Type type) {
return schema;
}

private static <T> T convertMapToType(Map<String, Object> map, Class<T> targetType) {
ObjectMapper mapper = new ObjectMapper();
JavaType javaType = mapper.getTypeFactory().constructType(targetType);
return mapper.convertValue(map, javaType);
}

private static <T> T convertMapToType(Map<String, Object> map, TypeReference<T> targetType) {
ObjectMapper mapper = new ObjectMapper();
JavaType javaType = mapper.getTypeFactory().constructType(targetType);
return mapper.convertValue(map, javaType);
}

// Sampling

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import java.util.function.Consumer;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
Expand Down Expand Up @@ -80,7 +78,7 @@ public <T> Optional<StructuredElicitResult<T>> elicit(Class<T> type) {
}

return Optional.of(new StructuredElicitResult<>(elicitResult.get().action(),
convertMapToType(elicitResult.get().content(), type), elicitResult.get().meta()));
JsonParser.convertMapToType(elicitResult.get().content(), type), elicitResult.get().meta()));
}

@Override
Expand All @@ -95,7 +93,7 @@ public <T> Optional<StructuredElicitResult<T>> elicit(TypeReference<T> type) {
}

return Optional.of(new StructuredElicitResult<>(elicitResult.get().action(),
convertMapToType(elicitResult.get().content(), type), elicitResult.get().meta()));
JsonParser.convertMapToType(elicitResult.get().content(), type), elicitResult.get().meta()));
}

@Override
Expand All @@ -114,7 +112,7 @@ public <T> Optional<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec>
}

return Optional.of(new StructuredElicitResult<>(elicitResult.get().action(),
convertMapToType(elicitResult.get().content(), returnType), elicitResult.get().meta()));
JsonParser.convertMapToType(elicitResult.get().content(), returnType), elicitResult.get().meta()));
}

@Override
Expand All @@ -134,7 +132,7 @@ public <T> Optional<StructuredElicitResult<T>> elicit(Consumer<ElicitationSpec>
}

return Optional.of(new StructuredElicitResult<>(elicitResult.get().action(),
convertMapToType(elicitResult.get().content(), returnType), elicitResult.get().meta()));
JsonParser.convertMapToType(elicitResult.get().content(), returnType), elicitResult.get().meta()));
}

@Override
Expand All @@ -157,6 +155,9 @@ private Optional<ElicitResult> elicitationInternal(String message, Type type, Ma
Assert.hasText(message, "Elicitation message must not be empty");
Assert.notNull(type, "Elicitation response type must not be null");

// TODO add validation for the Elicitation Schema
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#supported-schema-types

Map<String, Object> schema = typeSchemaCache.computeIfAbsent(type, t -> this.generateElicitSchema(t));

return this.elicit(ElicitRequest.builder().message(message).requestedSchema(schema).meta(meta).build());
Expand All @@ -169,18 +170,6 @@ private Map<String, Object> generateElicitSchema(Type type) {
return schema;
}

private static <T> T convertMapToType(Map<String, Object> map, Class<T> targetType) {
ObjectMapper mapper = new ObjectMapper();
JavaType javaType = mapper.getTypeFactory().constructType(targetType);
return mapper.convertValue(map, javaType);
}

private static <T> T convertMapToType(Map<String, Object> map, TypeReference<T> targetType) {
ObjectMapper mapper = new ObjectMapper();
JavaType javaType = mapper.getTypeFactory().constructType(targetType);
return mapper.convertValue(map, javaType);
}

// Sampling

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.modelcontextprotocol.spec.McpSchema.ResourceLink;
import io.modelcontextprotocol.spec.McpSchema.SamplingMessage;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.util.Assert;

/**
* @author Christian Tzolov
Expand Down Expand Up @@ -110,6 +111,11 @@ interface ProgressSpec {

ProgressSpec meta(String k, Object v);

default ProgressSpec percentage(int percentage) {
Assert.isTrue(percentage >= 0 && percentage <= 100, "Percentage must be between 0 and 100");
return this.progress(percentage).total(100.0);
}

}

// --------------------------------------
Expand Down Expand Up @@ -143,6 +149,7 @@ interface LoggingSpec {

ClientCapabilities clientCapabilities();

// TODO: Should we rename it to meta()?
Map<String, Object> requestMeta();

McpTransportContext transportContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public interface McpSyncRequestContext extends McpRequestContextTypes<McpSyncSer
// --------------------------------------
// Progress
// --------------------------------------
void progress(int progress);
void progress(int percentage);

void progress(Consumer<ProgressSpec> progressSpec);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2025-2025 the original author or authors.
*/

package org.springaicommunity.mcp.context;

import java.util.HashMap;
import java.util.Map;

import io.modelcontextprotocol.spec.McpSchema.ElicitResult.Action;
import io.modelcontextprotocol.util.Assert;

/**
* Builder for {@link StructuredElicitResult}.
*
* @param <T> the type of the structured content
* @author Christian Tzolov
*/
public class StructuredElicitResultBuilder<T> {

private Action action = Action.ACCEPT;

private T structuredContent;

private Map<String, Object> meta = new HashMap<>();

/**
* Private constructor to enforce builder pattern usage.
*/
private StructuredElicitResultBuilder() {
this.meta = new HashMap<>();
}

/**
* Creates a new builder instance.
* @param <T> the type of the structured content
* @return a new builder instance
*/
public static <T> StructuredElicitResultBuilder<T> builder() {
return new StructuredElicitResultBuilder<>();
}

/**
* Sets the action.
* @param action the action to set
* @return this builder instance
*/
public StructuredElicitResultBuilder<T> action(Action action) {
Assert.notNull(action, "Action must not be null");
this.action = action;
return this;
}

/**
* Sets the structured content.
* @param structuredContent the structured content to set
* @return this builder instance
*/
public StructuredElicitResultBuilder<T> structuredContent(T structuredContent) {
this.structuredContent = structuredContent;
return this;
}

/**
* Sets the meta map.
* @param meta the meta map to set
* @return this builder instance
*/
public StructuredElicitResultBuilder<T> meta(Map<String, Object> meta) {
this.meta = meta != null ? new HashMap<>(meta) : new HashMap<>();
return this;
}

/**
* Adds a single meta entry.
* @param key the meta key
* @param value the meta value
* @return this builder instance
*/
public StructuredElicitResultBuilder<T> addMeta(String key, Object value) {
this.meta.put(key, value);
return this;
}

/**
* Builds the {@link StructuredElicitResult} instance.
* @return a new StructuredElicitResult instance
*/
public StructuredElicitResult<T> build() {
return new StructuredElicitResult<>(this.action, this.structuredContent, this.meta);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import java.util.function.Function;

import org.springaicommunity.mcp.annotation.McpElicitation;

import org.springaicommunity.mcp.context.StructuredElicitResult;
import org.springaicommunity.mcp.method.tool.utils.JsonParser;
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -56,19 +57,34 @@ public Mono<ElicitResult> apply(ElicitRequest request) {

// If the method returns a Mono, handle it
if (result instanceof Mono) {
@SuppressWarnings("unchecked")
Mono<ElicitResult> monoResult = (Mono<ElicitResult>) result;
return monoResult;
}
// If the method returns an ElicitResult directly, wrap it in a Mono
else if (result instanceof ElicitResult) {
return Mono.just((ElicitResult) result);
Mono<?> monoResult = (Mono<?>) result;
return monoResult.flatMap(value -> {
if (value instanceof StructuredElicitResult) {
StructuredElicitResult<?> structuredElicitResult = (StructuredElicitResult<?>) value;

var content = structuredElicitResult.structuredContent() != null
? JsonParser.convertObjectToMap(structuredElicitResult.structuredContent()) : null;

return Mono.just(ElicitResult.builder()
.message(structuredElicitResult.action())
.content(content)
.meta(structuredElicitResult.meta())
.build());

}
else if (value instanceof ElicitResult) {
return Mono.just((ElicitResult) value);
}

return Mono.error(new McpElicitationMethodException(
"Method must return Mono<ElicitResult> or Mono<StructuredElicitResult>: "
+ this.method.getName()));

});
}
// Otherwise, throw an exception
else {
return Mono.error(new McpElicitationMethodException(
"Method must return Mono<ElicitResult> or ElicitResult: " + this.method.getName()));
}
return Mono.error(new McpElicitationMethodException(
"Method must return Mono<ElicitResult> or Mono<StructuredElicitResult>: " + this.method.getName()));
}
catch (Exception e) {
return Mono.error(new McpElicitationMethodException(
Expand All @@ -85,10 +101,10 @@ else if (result instanceof ElicitResult) {
protected void validateReturnType(Method method) {
Class<?> returnType = method.getReturnType();

if (!Mono.class.isAssignableFrom(returnType) && !ElicitResult.class.isAssignableFrom(returnType)) {
if (!Mono.class.isAssignableFrom(returnType)) {
throw new IllegalArgumentException(
"Method must return Mono<ElicitResult> or ElicitResult: " + method.getName() + " in "
+ method.getDeclaringClass().getName() + " returns " + returnType.getName());
"Method must return Mono<ElicitResult> or Mono<StructuredElicitResult>: " + method.getName()
+ " in " + method.getDeclaringClass().getName() + " returns " + returnType.getName());
}
}

Expand Down
Loading