Skip to content

Commit 8d5872f

Browse files
denniskawurektzolov
andcommitted
feat(McpSchema): CallToolResult and CallToolRequest usability improvements (#87)
- Add constructor to CallToolResult with one String entry - Add a new constructor to CallToolRequest that accepts JSON string arguments - Implement a builder pattern for CallToolResult with methods for adding content items - Add test coverage for new functionality Signed-off-by: Christian Tzolov <[email protected]> Co-authored-by: Christian Tzolov <[email protected]>
1 parent bda3cab commit 8d5872f

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

+111
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
1919
import com.fasterxml.jackson.core.type.TypeReference;
2020
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import io.modelcontextprotocol.util.Assert;
2122
import org.slf4j.Logger;
2223
import org.slf4j.LoggerFactory;
2324

@@ -741,6 +742,19 @@ private static JsonSchema parseSchema(String schema) {
741742
public record CallToolRequest(// @formatter:off
742743
@JsonProperty("name") String name,
743744
@JsonProperty("arguments") Map<String, Object> arguments) implements Request {
745+
746+
public CallToolRequest(String name, String jsonArguments) {
747+
this(name, parseJsonArguments(jsonArguments));
748+
}
749+
750+
private static Map<String, Object> parseJsonArguments(String jsonArguments) {
751+
try {
752+
return OBJECT_MAPPER.readValue(jsonArguments, MAP_TYPE_REF);
753+
}
754+
catch (IOException e) {
755+
throw new IllegalArgumentException("Invalid arguments: " + jsonArguments, e);
756+
}
757+
}
744758
}// @formatter:off
745759

746760
/**
@@ -756,6 +770,103 @@ public record CallToolRequest(// @formatter:off
756770
public record CallToolResult( // @formatter:off
757771
@JsonProperty("content") List<Content> content,
758772
@JsonProperty("isError") Boolean isError) {
773+
774+
/**
775+
* Creates a new instance of {@link CallToolResult} with a string containing the
776+
* tool result.
777+
*
778+
* @param content The content of the tool result. This will be mapped to a one-sized list
779+
* with a {@link TextContent} element.
780+
* @param isError If true, indicates that the tool execution failed and the content contains error information.
781+
* If false or absent, indicates successful execution.
782+
*/
783+
public CallToolResult(String content, Boolean isError) {
784+
this(List.of(new TextContent(content)), isError);
785+
}
786+
787+
/**
788+
* Creates a builder for {@link CallToolResult}.
789+
* @return a new builder instance
790+
*/
791+
public static Builder builder() {
792+
return new Builder();
793+
}
794+
795+
/**
796+
* Builder for {@link CallToolResult}.
797+
*/
798+
public static class Builder {
799+
private List<Content> content = new ArrayList<>();
800+
private Boolean isError;
801+
802+
/**
803+
* Sets the content list for the tool result.
804+
* @param content the content list
805+
* @return this builder
806+
*/
807+
public Builder content(List<Content> content) {
808+
Assert.notNull(content, "content must not be null");
809+
this.content = content;
810+
return this;
811+
}
812+
813+
/**
814+
* Sets the text content for the tool result.
815+
* @param textContent the text content
816+
* @return this builder
817+
*/
818+
public Builder textContent(List<String> textContent) {
819+
Assert.notNull(textContent, "textContent must not be null");
820+
textContent.stream()
821+
.map(TextContent::new)
822+
.forEach(this.content::add);
823+
return this;
824+
}
825+
826+
/**
827+
* Adds a content item to the tool result.
828+
* @param contentItem the content item to add
829+
* @return this builder
830+
*/
831+
public Builder addContent(Content contentItem) {
832+
Assert.notNull(contentItem, "contentItem must not be null");
833+
if (this.content == null) {
834+
this.content = new ArrayList<>();
835+
}
836+
this.content.add(contentItem);
837+
return this;
838+
}
839+
840+
/**
841+
* Adds a text content item to the tool result.
842+
* @param text the text content
843+
* @return this builder
844+
*/
845+
public Builder addTextContent(String text) {
846+
Assert.notNull(text, "text must not be null");
847+
return addContent(new TextContent(text));
848+
}
849+
850+
/**
851+
* Sets whether the tool execution resulted in an error.
852+
* @param isError true if the tool execution failed, false otherwise
853+
* @return this builder
854+
*/
855+
public Builder isError(Boolean isError) {
856+
Assert.notNull(isError, "isError must not be null");
857+
this.isError = isError;
858+
return this;
859+
}
860+
861+
/**
862+
* Builds a new {@link CallToolResult} instance.
863+
* @return a new CallToolResult instance
864+
*/
865+
public CallToolResult build() {
866+
return new CallToolResult(content, isError);
867+
}
868+
}
869+
759870
} // @formatter:on
760871

761872
// ---------------------------

mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java

+112
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Arrays;
77
import java.util.Collections;
88
import java.util.HashMap;
9+
import java.util.List;
910
import java.util.Map;
1011

1112
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -493,6 +494,25 @@ void testCallToolRequest() throws Exception {
493494
{"name":"test-tool","arguments":{"name":"test","value":42}}"""));
494495
}
495496

497+
@Test
498+
void testCallToolRequestJsonArguments() throws Exception {
499+
500+
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", """
501+
{
502+
"name": "test",
503+
"value": 42
504+
}
505+
""");
506+
507+
String value = mapper.writeValueAsString(request);
508+
509+
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
510+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
511+
.isObject()
512+
.isEqualTo(json("""
513+
{"name":"test-tool","arguments":{"name":"test","value":42}}"""));
514+
}
515+
496516
@Test
497517
void testCallToolResult() throws Exception {
498518
McpSchema.TextContent content = new McpSchema.TextContent("Tool execution result");
@@ -508,6 +528,98 @@ void testCallToolResult() throws Exception {
508528
{"content":[{"type":"text","text":"Tool execution result"}],"isError":false}"""));
509529
}
510530

531+
@Test
532+
void testCallToolResultBuilder() throws Exception {
533+
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
534+
.addTextContent("Tool execution result")
535+
.isError(false)
536+
.build();
537+
538+
String value = mapper.writeValueAsString(result);
539+
540+
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
541+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
542+
.isObject()
543+
.isEqualTo(json("""
544+
{"content":[{"type":"text","text":"Tool execution result"}],"isError":false}"""));
545+
}
546+
547+
@Test
548+
void testCallToolResultBuilderWithMultipleContents() throws Exception {
549+
McpSchema.TextContent textContent = new McpSchema.TextContent("Text result");
550+
McpSchema.ImageContent imageContent = new McpSchema.ImageContent(null, null, "base64data", "image/png");
551+
552+
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
553+
.addContent(textContent)
554+
.addContent(imageContent)
555+
.isError(false)
556+
.build();
557+
558+
String value = mapper.writeValueAsString(result);
559+
560+
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
561+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
562+
.isObject()
563+
.isEqualTo(
564+
json("""
565+
{"content":[{"type":"text","text":"Text result"},{"type":"image","data":"base64data","mimeType":"image/png"}],"isError":false}"""));
566+
}
567+
568+
@Test
569+
void testCallToolResultBuilderWithContentList() throws Exception {
570+
McpSchema.TextContent textContent = new McpSchema.TextContent("Text result");
571+
McpSchema.ImageContent imageContent = new McpSchema.ImageContent(null, null, "base64data", "image/png");
572+
List<McpSchema.Content> contents = Arrays.asList(textContent, imageContent);
573+
574+
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder().content(contents).isError(true).build();
575+
576+
String value = mapper.writeValueAsString(result);
577+
578+
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
579+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
580+
.isObject()
581+
.isEqualTo(
582+
json("""
583+
{"content":[{"type":"text","text":"Text result"},{"type":"image","data":"base64data","mimeType":"image/png"}],"isError":true}"""));
584+
}
585+
586+
@Test
587+
void testCallToolResultBuilderWithErrorResult() throws Exception {
588+
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
589+
.addTextContent("Error: Operation failed")
590+
.isError(true)
591+
.build();
592+
593+
String value = mapper.writeValueAsString(result);
594+
595+
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
596+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
597+
.isObject()
598+
.isEqualTo(json("""
599+
{"content":[{"type":"text","text":"Error: Operation failed"}],"isError":true}"""));
600+
}
601+
602+
@Test
603+
void testCallToolResultStringConstructor() throws Exception {
604+
// Test the existing string constructor alongside the builder
605+
McpSchema.CallToolResult result1 = new McpSchema.CallToolResult("Simple result", false);
606+
McpSchema.CallToolResult result2 = McpSchema.CallToolResult.builder()
607+
.addTextContent("Simple result")
608+
.isError(false)
609+
.build();
610+
611+
String value1 = mapper.writeValueAsString(result1);
612+
String value2 = mapper.writeValueAsString(result2);
613+
614+
// Both should produce the same JSON
615+
assertThat(value1).isEqualTo(value2);
616+
assertThatJson(value1).when(Option.IGNORING_ARRAY_ORDER)
617+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
618+
.isObject()
619+
.isEqualTo(json("""
620+
{"content":[{"type":"text","text":"Simple result"}],"isError":false}"""));
621+
}
622+
511623
// Sampling Tests
512624

513625
@Test

0 commit comments

Comments
 (0)