Skip to content
Open
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 @@ -1308,15 +1308,20 @@ private static Document adaptTimestamp(Document doc) {
return Document.of(EPOCH_SECONDS.readFromNumber(doc.asNumber()));
}

private Document adaptOutputDocument(Document doc, Schema schema) {
Document adaptOutputDocument(Document doc, Schema schema) {
if (doc == null) {
return null;
}
var toType = schema.type();
return switch (toType) {
case BIG_DECIMAL -> Document.of(doc.asBigDecimal().toString());
case BIG_INTEGER -> Document.of(doc.asBigInteger().toString());
case BLOB -> Document.of(Base64.getEncoder().encodeToString(ByteBufferUtils.getBytes(doc.asBlob())));
case BLOB -> switch (doc.type()) {
case STRING -> Document.of(doc.asString());
case BLOB -> Document.of(
Base64.getEncoder().encodeToString(ByteBufferUtils.getBytes(doc.asBlob())));
default -> badType(doc.type(), toType);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a incorrect fix and in the wrong layer. The premise of this fix is also incorrect

to a downstream Smithy service using RPC v2 CBOR, Blob fields in the response can be deserialized as string Documents (already base64-encoded) rather than typed blob Documents.

That's the representation over the wire not after materialization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual issue is Document.of(output) at McpService.java:704 round-trips JsonDocuments.StringDocument (has asBlob()) into core Documents.StringDocument (throws on asBlob()) before adaptOutputDocument resolves @OneOf document members containing blob fields. Should I fix it by skipping the round-trip (cast StructDocument directly since it implements Document), or add asBlob() to core Documents.StringDocument?

// Use adaptTimestamp() instead of asTimestamp() because oneOf union members are
// deserialized as untyped Documents (no schema available). Timestamps in these
// documents remain as strings or numbers rather than being converted to Timestamp Documents.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.mcp.server;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.core.serde.document.Document;
import software.amazon.smithy.model.shapes.ShapeId;

class McpServiceTest {

private McpService service;

@BeforeEach
void setUp() {
service = new McpService(
Collections.emptyMap(),
List.of(),
"test",
"1.0",
null,
null,
null
);
}

@Test
void testAdaptOutputBlobFromBinaryDocument() {
var schema = Schema.createBlob(ShapeId.from("smithy.test#Blob"));
var bytes = "Hello, World!".getBytes(StandardCharsets.UTF_8);

var result = service.adaptOutputDocument(Document.of(bytes), schema);

assertEquals(Base64.getEncoder().encodeToString(bytes), result.asString());
}

@Test
void testAdaptOutputBlobFromStringDocument() {
var schema = Schema.createBlob(ShapeId.from("smithy.test#Blob"));
var base64 = Base64.getEncoder().encodeToString("payload".getBytes(StandardCharsets.UTF_8));

var result = service.adaptOutputDocument(Document.of(base64), schema);

assertEquals(base64, result.asString());
}

@Test
void testAdaptOutputBlobFromUnsupportedDocumentTypeThrows() {
var schema = Schema.createBlob(ShapeId.from("smithy.test#Blob"));

assertThrows(Exception.class, () -> service.adaptOutputDocument(Document.of(42), schema));
}

@Test
void testAdaptOutputNullReturnsNull() {
var schema = Schema.createBlob(ShapeId.from("smithy.test#Blob"));

assertNull(service.adaptOutputDocument(null, schema));
}

@Test
void testAdaptOutputStructureWithBlobFields() {
var blobSchema = Schema.createBlob(ShapeId.from("smithy.test#Blob"));
var structSchema = Schema.structureBuilder(ShapeId.from("smithy.test#Output"))
.putMember("binaryBlob", blobSchema)
.putMember("stringBlob", blobSchema)
.build();

var bytes = "binary content".getBytes(StandardCharsets.UTF_8);
var base64 = Base64.getEncoder().encodeToString("string content".getBytes(StandardCharsets.UTF_8));
var doc = Document.of(Map.of(
"binaryBlob", Document.of(bytes),
"stringBlob", Document.of(base64)));

var result = service.adaptOutputDocument(doc, structSchema);

assertEquals(Base64.getEncoder().encodeToString(bytes), result.getMember("binaryBlob").asString());
assertEquals(base64, result.getMember("stringBlob").asString());
}
}