feat: add progress token support and extend Request interface (#352)

- Add PaginatedRequest and ReadResourceRequest to Request sealed interface
- Add default meta() and progressToken() methods to Request interface
- Add _meta parameter support to request classes:
  - ReadResourceRequest, GetPromptRequest, CallToolRequest
  - CreateMessageRequest, ElicitRequest, CompleteRequest, PaginatedRequest
- Add builder methods for progress token configuration
- Add message field to ProgressNotification
- Maintain backward compatibility with existing constructors
- Add _meta field to the InitialRequest record

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-06-29 09:29:48 +02:00
commit c0b398c650
2 changed files with 1321 additions and 847 deletions

File diff suppressed because it is too large Load diff

View file

@ -3,22 +3,23 @@
*/
package io.modelcontextprotocol.spec;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import net.javacrumbs.jsonunit.core.Option;
/**
@ -106,6 +107,43 @@ public class McpSchemaTests {
assertThat(audioContent.mimeType()).isEqualTo("audio/wav");
}
@Test
void testCreateMessageRequestWithMeta() throws Exception {
McpSchema.TextContent content = new McpSchema.TextContent("User message");
McpSchema.SamplingMessage message = new McpSchema.SamplingMessage(McpSchema.Role.USER, content);
McpSchema.ModelHint hint = new McpSchema.ModelHint("gpt-4");
McpSchema.ModelPreferences preferences = new McpSchema.ModelPreferences(Collections.singletonList(hint), 0.3,
0.7, 0.9);
Map<String, Object> metadata = new HashMap<>();
metadata.put("session", "test-session");
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "create-message-token-456");
McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest.builder()
.messages(Collections.singletonList(message))
.modelPreferences(preferences)
.systemPrompt("You are a helpful assistant")
.includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.THIS_SERVER)
.temperature(0.7)
.maxTokens(1000)
.stopSequences(Arrays.asList("STOP", "END"))
.metadata(metadata)
.meta(meta)
.build();
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.containsEntry("_meta", Map.of("progressToken", "create-message-token-456"));
// Test Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("create-message-token-456");
}
@Test
void testEmbeddedResource() throws Exception {
McpSchema.TextResourceContents resourceContents = new McpSchema.TextResourceContents("resource://test",
@ -444,6 +482,36 @@ public class McpSchemaTests {
{"uri":"resource://test"}"""));
}
@Test
void testReadResourceRequestWithMeta() throws Exception {
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "read-resource-token-123");
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test", meta);
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"uri":"resource://test","_meta":{"progressToken":"read-resource-token-123"}}"""));
// Test Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("read-resource-token-123");
}
@Test
void testReadResourceRequestDeserialization() throws Exception {
McpSchema.ReadResourceRequest request = mapper.readValue("""
{"uri":"resource://test","_meta":{"progressToken":"test-token"}}""",
McpSchema.ReadResourceRequest.class);
assertThat(request.uri()).isEqualTo("resource://test");
assertThat(request.meta()).containsEntry("progressToken", "test-token");
assertThat(request.progressToken()).isEqualTo("test-token");
}
@Test
void testReadResourceResult() throws Exception {
McpSchema.TextResourceContents contents1 = new McpSchema.TextResourceContents("resource://test1", "text/plain",
@ -529,6 +597,30 @@ public class McpSchemaTests {
.isEqualTo(request);
}
@Test
void testGetPromptRequestWithMeta() throws Exception {
Map<String, Object> arguments = new HashMap<>();
arguments.put("arg1", "value1");
arguments.put("arg2", 42);
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "token123");
McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("test-prompt", arguments, meta);
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-prompt","arguments":{"arg1":"value1","arg2":42},"_meta":{"progressToken":"token123"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("token123");
}
@Test
void testGetPromptResult() throws Exception {
McpSchema.TextContent content1 = new McpSchema.TextContent("System message");
@ -775,6 +867,65 @@ public class McpSchemaTests {
{"name":"test-tool","arguments":{"name":"test","value":42}}"""));
}
@Test
void testCallToolRequestWithMeta() throws Exception {
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
.name("test-tool")
.arguments(Map.of("name", "test", "value", 42))
.progressToken("tool-progress-123")
.build();
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-tool","arguments":{"name":"test","value":42},"_meta":{"progressToken":"tool-progress-123"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(Map.of("progressToken", "tool-progress-123"));
assertThat(request.progressToken()).isEqualTo("tool-progress-123");
}
@Test
void testCallToolRequestBuilderWithJsonArguments() throws Exception {
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "json-builder-789");
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder().name("test-tool").arguments("""
{
"name": "test",
"value": 42
}
""").meta(meta).build();
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-tool","arguments":{"name":"test","value":42},"_meta":{"progressToken":"json-builder-789"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("json-builder-789");
}
@Test
void testCallToolRequestBuilderNameRequired() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("name", "test");
McpSchema.CallToolRequest.Builder builder = McpSchema.CallToolRequest.builder().arguments(arguments);
assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("name must not be empty");
}
@Test
void testCallToolResult() throws Exception {
McpSchema.TextContent content = new McpSchema.TextContent("Tool execution result");
@ -991,6 +1142,141 @@ public class McpSchemaTests {
{"action":"accept","content":{"foo":"bar"}}"""));
}
@Test
void testElicitRequestWithMeta() throws Exception {
Map<String, Object> requestedSchema = Map.of("type", "object", "required", List.of("name"), "properties",
Map.of("name", Map.of("type", "string")));
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "elicit-token-789");
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder()
.message("Please provide your name")
.requestedSchema(requestedSchema)
.meta(meta)
.build();
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789"));
// Test Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("elicit-token-789");
}
// Pagination Tests
@Test
void testPaginatedRequestNoArgs() throws Exception {
McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest();
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isNull();
assertThat(request.progressToken()).isNull();
}
@Test
void testPaginatedRequestWithCursor() throws Exception {
McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest("cursor123");
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"cursor":"cursor123"}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isNull();
assertThat(request.progressToken()).isNull();
}
@Test
void testPaginatedRequestWithMeta() throws Exception {
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "pagination-progress-456");
McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest("cursor123", meta);
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"cursor":"cursor123","_meta":{"progressToken":"pagination-progress-456"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("pagination-progress-456");
}
@Test
void testPaginatedRequestDeserialization() throws Exception {
McpSchema.PaginatedRequest request = mapper.readValue("""
{"cursor":"test-cursor","_meta":{"progressToken":"test-token"}}""", McpSchema.PaginatedRequest.class);
assertThat(request.cursor()).isEqualTo("test-cursor");
assertThat(request.meta()).containsEntry("progressToken", "test-token");
assertThat(request.progressToken()).isEqualTo("test-token");
}
// Complete Request Tests
@Test
void testCompleteRequest() throws Exception {
McpSchema.PromptReference promptRef = new McpSchema.PromptReference("test-prompt");
McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument("arg1",
"partial-value");
McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(promptRef, argument);
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"ref":{"type":"ref/prompt","name":"test-prompt"},"argument":{"name":"arg1","value":"partial-value"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isNull();
assertThat(request.progressToken()).isNull();
}
@Test
void testCompleteRequestWithMeta() throws Exception {
McpSchema.ResourceReference resourceRef = new McpSchema.ResourceReference("file:///test.txt");
McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument("path",
"/partial/path");
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "complete-progress-789");
McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(resourceRef, argument, meta);
String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"ref":{"type":"ref/resource","uri":"file:///test.txt"},"argument":{"name":"path","value":"/partial/path"},"_meta":{"progressToken":"complete-progress-789"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("complete-progress-789");
}
// Roots Tests
@Test
@ -1024,4 +1310,45 @@ public class McpSchemaTests {
}
// Progress Notification Tests
@Test
void testProgressNotificationWithMessage() throws Exception {
McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-123", 0.5, 1.0,
"Processing file 1 of 2");
String value = mapper.writeValueAsString(notification);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"progressToken":"progress-token-123","progress":0.5,"total":1.0,"message":"Processing file 1 of 2"}"""));
}
@Test
void testProgressNotificationDeserialization() throws Exception {
McpSchema.ProgressNotification notification = mapper.readValue("""
{"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done"}""",
McpSchema.ProgressNotification.class);
assertThat(notification.progressToken()).isEqualTo("token-456");
assertThat(notification.progress()).isEqualTo(0.75);
assertThat(notification.total()).isEqualTo(1.0);
assertThat(notification.message()).isEqualTo("Almost done");
}
@Test
void testProgressNotificationWithoutMessage() throws Exception {
McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-789", 0.25,
null, null);
String value = mapper.writeValueAsString(notification);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"progressToken":"progress-token-789","progress":0.25}"""));
}
}