This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
commit aeba30dfd27be32c1ef141426fce3097cec56ab0 Author: James Bognar <[email protected]> AuthorDate: Tue Jul 28 16:39:22 2026 -0400 feat: add revision-neutral MCP value types (schema, role, content blocks) Co-authored-by: Cursor <[email protected]> --- .../juneau/rest/server/mcp/McpContentBlock.java | 128 +++++++++++++++++++++ .../rest/server/mcp/McpResourceContents.java | 118 +++++++++++++++++++ .../org/apache/juneau/rest/server/mcp/McpRole.java | 58 ++++++++++ .../apache/juneau/rest/server/mcp/McpSchema.java | 59 ++++++++++ .../rest/server/mcp/McpNeutralValues_Test.java | 98 ++++++++++++++++ 5 files changed, 461 insertions(+) diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpContentBlock.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpContentBlock.java new file mode 100644 index 0000000000..f18c97b9bb --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpContentBlock.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.juneau.rest.server.mcp; + +/** + * Revision-neutral content block returned by tool calls and prompt messages. + * + * <p> + * Carries all three variants supported today — text, image, and embedded resource — so a handler + * cannot express less through the neutral model than it can through a wire bean. Supersedes the + * wire-level {@code Content} dictionary and its three leaf types. + */ +public final class McpContentBlock { + + /** Which content variant is populated. */ + public enum Kind { + /** {@link McpContentBlock#text()} is populated. */ + TEXT, + /** {@link McpContentBlock#data()} and {@link McpContentBlock#mimeType()} are populated. */ + IMAGE, + /** {@link McpContentBlock#resource()} is populated. */ + RESOURCE + } + + private final Kind kind; + private final String text; + private final String data; + private final String mimeType; + private final McpResourceContents resource; + + private McpContentBlock(Kind kind, String text, String data, String mimeType, McpResourceContents resource) { + this.kind = kind; + this.text = text; + this.data = data; + this.mimeType = mimeType; + this.resource = resource; + } + + /** + * Creates a text content block. + * + * @param text The text. Can be <jk>null</jk>. + * @return A new block. Never <jk>null</jk>. + */ + public static McpContentBlock text(String text) { + return new McpContentBlock(Kind.TEXT, text, null, null, null); + } + + /** + * Creates an image content block. + * + * @param data Base64-encoded image bytes. Can be <jk>null</jk>. + * @param mimeType The image media type. Can be <jk>null</jk>. + * @return A new block. Never <jk>null</jk>. + */ + public static McpContentBlock image(String data, String mimeType) { + return new McpContentBlock(Kind.IMAGE, null, data, mimeType, null); + } + + /** + * Creates an embedded-resource content block. + * + * @param resource The embedded payload. Can be <jk>null</jk>. + * @return A new block. Never <jk>null</jk>. + */ + public static McpContentBlock resource(McpResourceContents resource) { + return new McpContentBlock(Kind.RESOURCE, null, null, null, resource); + } + + /** + * Which variant this block carries. + * + * @return The variant. Never <jk>null</jk>. + */ + public Kind kind() { + return kind; + } + + /** + * The text payload. + * + * @return The text, or <jk>null</jk> unless this is a {@link Kind#TEXT} block. + */ + public String text() { + return text; + } + + /** + * The base64 image payload. + * + * @return The data, or <jk>null</jk> unless this is a {@link Kind#IMAGE} block. + */ + public String data() { + return data; + } + + /** + * The image media type. + * + * @return The media type, or <jk>null</jk> unless this is a {@link Kind#IMAGE} block. + */ + public String mimeType() { + return mimeType; + } + + /** + * The embedded resource payload. + * + * @return The payload, or <jk>null</jk> unless this is a {@link Kind#RESOURCE} block. + */ + public McpResourceContents resource() { + return resource; + } +} diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceContents.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceContents.java new file mode 100644 index 0000000000..550231ff00 --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceContents.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.juneau.rest.server.mcp; + +/** + * Revision-neutral resource payload, either inline text or a base64 blob. + * + * <p> + * Supersedes the wire-level {@code ResourceContents} dictionary and its two leaf types. Used both + * by {@link McpContentBlock#resource(McpResourceContents)} and by {@link McpResourceOutcome}. + */ +public final class McpResourceContents { + + /** Which payload variant is populated. */ + public enum Kind { + /** {@link McpResourceContents#text()} is populated. */ + TEXT, + /** {@link McpResourceContents#blob()} is populated. */ + BLOB + } + + private final Kind kind; + private final String uri; + private final String mimeType; + private final String text; + private final String blob; + + private McpResourceContents(Kind kind, String uri, String mimeType, String text, String blob) { + this.kind = kind; + this.uri = uri; + this.mimeType = mimeType; + this.text = text; + this.blob = blob; + } + + /** + * Creates an inline-text resource payload. + * + * @param uri The resource URI. Can be <jk>null</jk>. + * @param mimeType The media type. Can be <jk>null</jk>. + * @param text The text payload. Can be <jk>null</jk>. + * @return A new payload. Never <jk>null</jk>. + */ + public static McpResourceContents text(String uri, String mimeType, String text) { + return new McpResourceContents(Kind.TEXT, uri, mimeType, text, null); + } + + /** + * Creates a base64-blob resource payload. + * + * @param uri The resource URI. Can be <jk>null</jk>. + * @param mimeType The media type. Can be <jk>null</jk>. + * @param blob The base64-encoded payload. Can be <jk>null</jk>. + * @return A new payload. Never <jk>null</jk>. + */ + public static McpResourceContents blob(String uri, String mimeType, String blob) { + return new McpResourceContents(Kind.BLOB, uri, mimeType, null, blob); + } + + /** + * Which variant this payload carries. + * + * @return The variant. Never <jk>null</jk>. + */ + public Kind kind() { + return kind; + } + + /** + * The resource URI. + * + * @return The URI, or <jk>null</jk> if not set. + */ + public String uri() { + return uri; + } + + /** + * The media type. + * + * @return The media type, or <jk>null</jk> if not set. + */ + public String mimeType() { + return mimeType; + } + + /** + * The inline text payload. + * + * @return The text, or <jk>null</jk> for a {@link Kind#BLOB} payload. + */ + public String text() { + return text; + } + + /** + * The base64-encoded payload. + * + * @return The blob, or <jk>null</jk> for a {@link Kind#TEXT} payload. + */ + public String blob() { + return blob; + } +} diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRole.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRole.java new file mode 100644 index 0000000000..751bed77c9 --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRole.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.juneau.rest.server.mcp; + +/** + * Revision-neutral role for {@link McpPromptMessage} entries. + * + * <p> + * MCP wire formats use lowercase strings; {@link #toWire()} and {@link #toString()} both return them. + */ +public enum McpRole { + + /** User role. */ + USER("user"), + + /** Assistant role. */ + ASSISTANT("assistant"), + + /** System role. */ + SYSTEM("system"), + + /** Tool role. */ + TOOL("tool"); + + private final String wire; + + McpRole(String wire) { + this.wire = wire; + } + + /** + * Wire token for JSON payloads. + * + * @return Lowercase MCP role string. + */ + public String toWire() { + return wire; + } + + @Override /* Object */ + public String toString() { + return wire; + } +} diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpSchema.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpSchema.java new file mode 100644 index 0000000000..537e9ddc09 --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpSchema.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.juneau.rest.server.mcp; + +import org.apache.juneau.marshall.collections.*; + +/** + * Revision-neutral carrier for a tool's input schema. + * + * <p> + * Deliberately a carrier, not a structured bean: the core has no opinion about which JSON Schema + * dialect or keyword set a revision supports. Each revision's adapter is responsible for validating + * that the carried map is expressible in its own wire schema type, and for rejecting configurations + * that are not. + * + * <p> + * The supplied map is <em>not</em> copied; callers must not mutate a map after handing it over. + */ +public final class McpSchema { + + private final JsonMap raw; + + private McpSchema(JsonMap raw) { + this.raw = raw; + } + + /** + * Creates a schema carrier around a raw JSON Schema object. + * + * @param raw The schema as a JSON object. Can be <jk>null</jk>, which yields an empty schema. + * @return A new carrier. Never <jk>null</jk>. + */ + public static McpSchema of(JsonMap raw) { + return new McpSchema(raw == null ? new JsonMap() : raw); + } + + /** + * The carried schema. + * + * @return The schema map. Never <jk>null</jk>; not a copy. + */ + public JsonMap toJsonMap() { + return raw; + } +} diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpNeutralValues_Test.java b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpNeutralValues_Test.java new file mode 100644 index 0000000000..c5f6e12fe7 --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpNeutralValues_Test.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.juneau.rest.server.mcp; + +import static org.junit.jupiter.api.Assertions.*; + +import org.apache.juneau.marshall.collections.*; +import org.junit.jupiter.api.*; + +/** + * Coverage for the revision-neutral value types: {@link McpSchema}, {@link McpRole}, + * {@link McpContentBlock}, {@link McpResourceContents}. + */ +class McpNeutralValues_Test { + + @Test + void a01_schema_carriesRawMapThrough() { + var a = JsonMap.of("type", "object"); + var b = McpSchema.of(a); + assertSame(a, b.toJsonMap()); + } + + @Test + void a02_schema_nullBecomesEmptyMap() { + assertTrue(McpSchema.of(null).toJsonMap().isEmpty()); + } + + @Test + void b01_role_hasFourValuesWithLowercaseWire() { + assertEquals(4, McpRole.values().length); + assertEquals("user", McpRole.USER.toWire()); + assertEquals("assistant", McpRole.ASSISTANT.toWire()); + assertEquals("system", McpRole.SYSTEM.toWire()); + assertEquals("tool", McpRole.TOOL.toWire()); + for (var a : McpRole.values()) + assertEquals(a.toWire(), a.toString()); + } + + @Test + void c01_resourceContents_text() { + var a = McpResourceContents.text("file:///a", "text/plain", "body"); + assertEquals(McpResourceContents.Kind.TEXT, a.kind()); + assertEquals("file:///a", a.uri()); + assertEquals("text/plain", a.mimeType()); + assertEquals("body", a.text()); + assertNull(a.blob()); + } + + @Test + void c02_resourceContents_blob() { + var a = McpResourceContents.blob("file:///b", "application/octet-stream", "Qk09"); + assertEquals(McpResourceContents.Kind.BLOB, a.kind()); + assertEquals("Qk09", a.blob()); + assertNull(a.text()); + } + + @Test + void d01_contentBlock_text() { + var a = McpContentBlock.text("hi"); + assertEquals(McpContentBlock.Kind.TEXT, a.kind()); + assertEquals("hi", a.text()); + assertNull(a.data()); + assertNull(a.mimeType()); + assertNull(a.resource()); + } + + @Test + void d02_contentBlock_image() { + var a = McpContentBlock.image("AAA=", "image/png"); + assertEquals(McpContentBlock.Kind.IMAGE, a.kind()); + assertEquals("AAA=", a.data()); + assertEquals("image/png", a.mimeType()); + assertNull(a.text()); + } + + @Test + void d03_contentBlock_resource() { + var a = McpResourceContents.text("file:///a", null, "x"); + var b = McpContentBlock.resource(a); + assertEquals(McpContentBlock.Kind.RESOURCE, b.kind()); + assertSame(a, b.resource()); + assertNull(b.text()); + } +}
