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,6 +9,8 @@
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -155,9 +157,9 @@ private void handleRequest(JsonRpcRequest req) {
} else {
// Handle locally
var operation = tool.operation();
var input = req.getParams()
.getMember("arguments")
.asShape(operation.getApiOperation().inputBuilder());
var argumentsDoc = req.getParams().getMember("arguments");
var adaptedDoc = adaptDocument(argumentsDoc, operation.getApiOperation().inputSchema());
var input = adaptedDoc.asShape(operation.getApiOperation().inputBuilder());
var output = operation.function().apply(input, null);
var result = CallToolResult.builder()
.content(List.of(TextContent.builder()
Expand Down Expand Up @@ -417,14 +419,14 @@ public void awaitCompletion() throws InterruptedException {
done.await();
}

private record Tool(ToolInfo toolInfo, Operation operation, McpServerProxy proxy) {
private record Tool(ToolInfo toolInfo, Operation operation, McpServerProxy proxy, boolean requiredAdapting) {

Tool(ToolInfo toolInfo, Operation operation) {
this(toolInfo, operation, null);
this(toolInfo, operation, null, false);
}

Tool(ToolInfo toolInfo, McpServerProxy proxy) {
this(toolInfo, null, proxy);
this(toolInfo, null, proxy, false);
}
}

Expand All @@ -436,6 +438,62 @@ private static String appendSentences(String first, String second) {
return first + second;
}

private static Document adaptDocument(Document doc, Schema schema) {
var fromType = doc.type();
var toType = schema.type();
return switch (toType) {
case BIG_DECIMAL -> switch (fromType) {
case STRING -> Document.of(new BigDecimal(doc.asString()));
case BIG_INTEGER -> doc;
default -> badType(fromType, toType);
};
case BIG_INTEGER ->
switch (fromType) {
case STRING -> Document.of(new BigInteger(doc.asString()));
case BIG_INTEGER -> doc;
default -> badType(fromType, toType);
};
case BLOB -> switch (fromType) {
case STRING -> Document.of(doc.asString().getBytes(StandardCharsets.UTF_8));
case BLOB -> doc;
default -> badType(fromType, toType);
};
case STRUCTURE, UNION -> {
var convertedMembers = new HashMap<String, Document>();
var members = schema.members();
for (var member : members) {
var memberName = member.memberName();
var memberDoc = doc.getMember(memberName);
if (memberDoc != null) {
convertedMembers.put(memberName, adaptDocument(memberDoc, member.memberTarget()));
}
}
yield Document.of(convertedMembers);
}
case LIST, SET -> {
var listMember = schema.listMember();
var convertedList = new ArrayList<Document>();
for (var item : doc.asList()) {
convertedList.add(adaptDocument(item, listMember.memberTarget()));
}
yield Document.of(convertedList);
}
case MAP -> {
var mapValue = schema.mapValueMember();
var convertedMap = new HashMap<String, Document>();
for (var entry : doc.asStringMap().entrySet()) {
convertedMap.put(entry.getKey(), adaptDocument(entry.getValue(), mapValue.memberTarget()));
}
yield Document.of(convertedMap);
}
default -> doc;
};
}

private static Document badType(ShapeType from, ShapeType to) {
throw new RuntimeException("Cannot convert from " + from + " to " + to);
}

public static McpServerBuilder builder() {
return new McpServerBuilder();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@
package software.amazon.smithy.java.mcp.server;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor;
import software.amazon.smithy.java.client.core.interceptors.InputHook;
import software.amazon.smithy.java.core.serde.document.Document;
import software.amazon.smithy.java.dynamicschemas.StructDocument;
import software.amazon.smithy.java.json.JsonCodec;
import software.amazon.smithy.java.json.JsonSettings;
import software.amazon.smithy.java.mcp.model.JsonRpcRequest;
Expand All @@ -21,6 +30,7 @@
import software.amazon.smithy.java.server.Server;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ShapeType;

public class McpServerTest {
private static final JsonCodec CODEC = JsonCodec.builder()
Expand Down Expand Up @@ -84,6 +94,9 @@ public void validateToolStructure() {
var list = properties.get("list").asStringMap();
assertEquals("array", list.get("type").asString());

var bigDecimal = properties.get("bigDecimalField").asStringMap();
assertEquals("string", bigDecimal.get("type").asString());

var listItems = list.get("items").asStringMap();
assertEquals("object", listItems.get("type").asString());
var listItemProperties = listItems.get("properties").asStringMap();
Expand All @@ -106,6 +119,111 @@ public void validateToolStructure() {
validateNestedStructure(doubleNestedProperties);
}

@Test
void testInputAdaptation() {
AtomicReference<StructDocument> capturedInput = new AtomicReference<>();
server = McpServer.builder()
.input(input)
.output(output)
.addService(ProxyService.builder()
.service(ShapeId.from("smithy.test#TestService"))
.proxyEndpoint("http://localhost")
.clientConfigurator(
clientConfigurator -> clientConfigurator.addInterceptor(new ClientInterceptor() {
@Override
public void readBeforeSerialization(InputHook<?, ?> hook) {
capturedInput.set((StructDocument) hook.input());
}
}))
.model(MODEL)
.build())
.build();

server.start();

var bigDecimalValue = BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.TEN);
var bigIntegerValue = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.valueOf(100));
var blobValue = "Hello, World!";
var nestedBigDecimalValue = new BigDecimal("123.456");
var nestedBigIntegerValue = new BigInteger("9876543210");
var nestedBlobValue = "Nested blob content";

write("tools/call",
Document.of(
Map.of("name",
Document.of("TestOperation"),
"arguments",
Document.of(Map.of(
"bigDecimalField",
Document.of(bigDecimalValue.toString()),
"bigIntegerField",
Document.of(bigIntegerValue.toString()),
"blobField",
Document.of(blobValue),
"nestedWithBigNumbers",
Document.of(Map.of(
"nestedBigDecimal",
Document.of(nestedBigDecimalValue.toString()),
"nestedBigInteger",
Document.of(nestedBigIntegerValue.toString()),
"nestedBlob",
Document.of(nestedBlobValue),
"bigDecimalList",
Document.of(List.of(
Document.of("100.25"),
Document.of("200.75"))))))))));
assertNotNull(read());
var inputDocument = capturedInput.get();

var bigDecimalField = inputDocument.getMember("bigDecimalField");
assertNotNull(bigDecimalField);
assertEquals(ShapeType.BIG_DECIMAL, bigDecimalField.type());
assertEquals(bigDecimalValue, bigDecimalField.asBigDecimal());

var bigIntegerField = inputDocument.getMember("bigIntegerField");
assertNotNull(bigIntegerField);
assertEquals(ShapeType.BIG_INTEGER, bigIntegerField.type());
assertEquals(bigIntegerValue, bigIntegerField.asBigInteger());

var blobField = inputDocument.getMember("blobField");
assertNotNull(blobField);
assertEquals(ShapeType.BLOB, blobField.type());
assertEquals(blobValue, new String(blobField.asBlob().array(), StandardCharsets.UTF_8));

var nestedWithBigNumbers = inputDocument.getMember("nestedWithBigNumbers");
assertNotNull(nestedWithBigNumbers);
assertEquals(ShapeType.STRUCTURE, nestedWithBigNumbers.type());

var nestedStruct = (StructDocument) nestedWithBigNumbers;

var nestedBigDecimalField = nestedStruct.getMember("nestedBigDecimal");
assertNotNull(nestedBigDecimalField);
assertEquals(ShapeType.BIG_DECIMAL, nestedBigDecimalField.type());
assertEquals(nestedBigDecimalValue, nestedBigDecimalField.asBigDecimal());

var nestedBigIntegerField = nestedStruct.getMember("nestedBigInteger");
assertNotNull(nestedBigIntegerField);
assertEquals(ShapeType.BIG_INTEGER, nestedBigIntegerField.type());
assertEquals(nestedBigIntegerValue, nestedBigIntegerField.asBigInteger());

var nestedBlobField = nestedStruct.getMember("nestedBlob");
assertNotNull(nestedBlobField);
assertEquals(ShapeType.BLOB, nestedBlobField.type());
assertEquals(nestedBlobValue, new String(nestedBlobField.asBlob().array(), StandardCharsets.UTF_8));

var bigDecimalListField = nestedStruct.getMember("bigDecimalList");
assertNotNull(bigDecimalListField);
assertEquals(ShapeType.LIST, bigDecimalListField.type());
var bigDecimalList = bigDecimalListField.asList();
assertEquals(2, bigDecimalList.size());
assertEquals(ShapeType.BIG_DECIMAL, bigDecimalList.get(0).type());
assertEquals(ShapeType.BIG_DECIMAL, bigDecimalList.get(1).type());
assertEquals(new BigDecimal("100.25"), bigDecimalList.get(0).asBigDecimal());
assertEquals(new BigDecimal("200.75"), bigDecimalList.get(1).asBigDecimal());

server.shutdown().join();
}

private void validateNestedStructure(Map<String, Document> properties) {
var nestedStr = properties.get("nestedStr").asStringMap();
assertEquals("string", nestedStr.get("type").asString());
Expand Down Expand Up @@ -154,6 +272,7 @@ private JsonRpcResponse read() {
/// A TestOperation
operation TestOperation {
input: TestInput
output: TestInput
}

/// An input for TestOperation with a nested member
Expand All @@ -167,6 +286,14 @@ private JsonRpcResponse read() {
list: NestedList

doubleNestedList: DoubleNestedList

bigDecimalField: BigDecimal

bigIntegerField: BigInteger

blobField: Blob

nestedWithBigNumbers: NestedWithBigNumbers
}

list NestedList {
Expand All @@ -193,6 +320,25 @@ private JsonRpcResponse read() {
structure Recursive {
/// the nested field that points back to us
nested: Nested
}

/// A structure containing big number types
structure NestedWithBigNumbers {
/// A nested BigDecimal
nestedBigDecimal: BigDecimal

/// A nested BigInteger
nestedBigInteger: BigInteger

/// A nested Blob
nestedBlob: Blob

/// A list of BigDecimals
bigDecimalList: BigDecimalList
}

list BigDecimalList {
member: BigDecimal
}""";

private static final Model MODEL = Model.assembler()
Expand Down