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:
parent
d7bcb237b8
commit
c0b398c650
2 changed files with 1321 additions and 847 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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}"""));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue