feat: Add _meta support to MCPSchema (#368)

- Add _meta field to all MCP schema records for extensible metadata support
- Introduce Result and Notification sealed interfaces for better type organization
- Update Request interface to include SubscribeRequest and UnsubscribeRequest
- Add backwards-compatible constructors for existing record types
- Enhance test coverage for metadata field functionality

Breaking change: Modifyibng the Request seald inteface is a breaking change.
This commit is contained in:
Anurag Pant 2025-07-01 11:27:07 -07:00 • committed by Christian Tzolov
commit 87cdaf883a
2 changed files with 424 additions and 79 deletions

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@ import net.javacrumbs.jsonunit.core.Option;
/** /**
* @author Christian Tzolov * @author Christian Tzolov
* @author Anurag Pant
*/ */
public class McpSchemaTests { public class McpSchemaTests {
@ -46,11 +47,12 @@ public class McpSchemaTests {
@Test @Test
void testTextContentDeserialization() throws Exception { void testTextContentDeserialization() throws Exception {
McpSchema.TextContent textContent = mapper.readValue(""" McpSchema.TextContent textContent = mapper.readValue("""
{"type":"text","text":"XXX"}""", McpSchema.TextContent.class); {"type":"text","text":"XXX","_meta":{"metaKey":"metaValue"}}""", McpSchema.TextContent.class);
assertThat(textContent).isNotNull(); assertThat(textContent).isNotNull();
assertThat(textContent.type()).isEqualTo("text"); assertThat(textContent.type()).isEqualTo("text");
assertThat(textContent.text()).isEqualTo("XXX"); assertThat(textContent.text()).isEqualTo("XXX");
assertThat(textContent.meta()).containsKey("metaKey");
} }
@Test @Test
@ -78,11 +80,13 @@ public class McpSchemaTests {
@Test @Test
void testImageContentDeserialization() throws Exception { void testImageContentDeserialization() throws Exception {
McpSchema.ImageContent imageContent = mapper.readValue(""" McpSchema.ImageContent imageContent = mapper.readValue("""
{"type":"image","data":"base64encodeddata","mimeType":"image/png"}""", McpSchema.ImageContent.class); {"type":"image","data":"base64encodeddata","mimeType":"image/png","_meta":{"metaKey":"metaValue"}}""",
McpSchema.ImageContent.class);
assertThat(imageContent).isNotNull(); assertThat(imageContent).isNotNull();
assertThat(imageContent.type()).isEqualTo("image"); assertThat(imageContent.type()).isEqualTo("image");
assertThat(imageContent.data()).isEqualTo("base64encodeddata"); assertThat(imageContent.data()).isEqualTo("base64encodeddata");
assertThat(imageContent.mimeType()).isEqualTo("image/png"); assertThat(imageContent.mimeType()).isEqualTo("image/png");
assertThat(imageContent.meta()).containsKey("metaKey");
} }
@Test @Test
@ -100,11 +104,13 @@ public class McpSchemaTests {
@Test @Test
void testAudioContentDeserialization() throws Exception { void testAudioContentDeserialization() throws Exception {
McpSchema.AudioContent audioContent = mapper.readValue(""" McpSchema.AudioContent audioContent = mapper.readValue("""
{"type":"audio","data":"base64encodeddata","mimeType":"audio/wav"}""", McpSchema.AudioContent.class); {"type":"audio","data":"base64encodeddata","mimeType":"audio/wav","_meta":{"metaKey":"metaValue"}}""",
McpSchema.AudioContent.class);
assertThat(audioContent).isNotNull(); assertThat(audioContent).isNotNull();
assertThat(audioContent.type()).isEqualTo("audio"); assertThat(audioContent.type()).isEqualTo("audio");
assertThat(audioContent.data()).isEqualTo("base64encodeddata"); assertThat(audioContent.data()).isEqualTo("base64encodeddata");
assertThat(audioContent.mimeType()).isEqualTo("audio/wav"); assertThat(audioContent.mimeType()).isEqualTo("audio/wav");
assertThat(audioContent.meta()).containsKey("metaKey");
} }
@Test @Test
@ -164,7 +170,7 @@ public class McpSchemaTests {
void testEmbeddedResourceDeserialization() throws Exception { void testEmbeddedResourceDeserialization() throws Exception {
McpSchema.EmbeddedResource embeddedResource = mapper.readValue( McpSchema.EmbeddedResource embeddedResource = mapper.readValue(
""" """
{"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}""", {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"},"_meta":{"metaKey":"metaValue"}}""",
McpSchema.EmbeddedResource.class); McpSchema.EmbeddedResource.class);
assertThat(embeddedResource).isNotNull(); assertThat(embeddedResource).isNotNull();
assertThat(embeddedResource.type()).isEqualTo("resource"); assertThat(embeddedResource.type()).isEqualTo("resource");
@ -172,6 +178,7 @@ public class McpSchemaTests {
assertThat(embeddedResource.resource().uri()).isEqualTo("resource://test"); assertThat(embeddedResource.resource().uri()).isEqualTo("resource://test");
assertThat(embeddedResource.resource().mimeType()).isEqualTo("text/plain"); assertThat(embeddedResource.resource().mimeType()).isEqualTo("text/plain");
assertThat(((TextResourceContents) embeddedResource.resource()).text()).isEqualTo("Sample resource content"); assertThat(((TextResourceContents) embeddedResource.resource()).text()).isEqualTo("Sample resource content");
assertThat(embeddedResource.meta()).containsKey("metaKey");
} }
@Test @Test
@ -194,7 +201,7 @@ public class McpSchemaTests {
void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception {
McpSchema.EmbeddedResource embeddedResource = mapper.readValue( McpSchema.EmbeddedResource embeddedResource = mapper.readValue(
""" """
{"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}""", {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob","_meta":{"metaKey":"metaValue"}}}""",
McpSchema.EmbeddedResource.class); McpSchema.EmbeddedResource.class);
assertThat(embeddedResource).isNotNull(); assertThat(embeddedResource).isNotNull();
assertThat(embeddedResource.type()).isEqualTo("resource"); assertThat(embeddedResource.type()).isEqualTo("resource");
@ -203,12 +210,14 @@ public class McpSchemaTests {
assertThat(embeddedResource.resource().mimeType()).isEqualTo("application/octet-stream"); assertThat(embeddedResource.resource().mimeType()).isEqualTo("application/octet-stream");
assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).blob()) assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).blob())
.isEqualTo("base64encodedblob"); .isEqualTo("base64encodedblob");
assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).meta()).containsKey("metaKey");
} }
@Test @Test
void testResourceLink() throws Exception { void testResourceLink() throws Exception {
McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", "Main file", McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", "Main file",
"file:///project/src/main.rs", "Primary application entry point", "text/x-rust", null, null); "file:///project/src/main.rs", "Primary application entry point", "text/x-rust", null, null,
Map.of("metaKey", "metaValue"));
String value = mapper.writeValueAsString(resourceLink); String value = mapper.writeValueAsString(resourceLink);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -216,14 +225,14 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"type":"resource_link","name":"main.rs","title":"Main file","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""")); {"type":"resource_link","name":"main.rs","title":"Main file","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust","_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
void testResourceLinkDeserialization() throws Exception { void testResourceLinkDeserialization() throws Exception {
McpSchema.ResourceLink resourceLink = mapper.readValue( McpSchema.ResourceLink resourceLink = mapper.readValue(
""" """
{"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""", {"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust","_meta":{"metaKey":"metaValue"}}""",
McpSchema.ResourceLink.class); McpSchema.ResourceLink.class);
assertThat(resourceLink).isNotNull(); assertThat(resourceLink).isNotNull();
assertThat(resourceLink.type()).isEqualTo("resource_link"); assertThat(resourceLink.type()).isEqualTo("resource_link");
@ -231,6 +240,7 @@ public class McpSchemaTests {
assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs"); assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs");
assertThat(resourceLink.description()).isEqualTo("Primary application entry point"); assertThat(resourceLink.description()).isEqualTo("Primary application entry point");
assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust"); assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust");
assertThat(resourceLink.meta()).containsEntry("metaKey", "metaValue");
} }
// JSON-RPC Message Types Tests // JSON-RPC Message Types Tests
@ -307,8 +317,10 @@ public class McpSchemaTests {
.build(); .build();
McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0");
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.InitializeRequest request = new McpSchema.InitializeRequest("2024-11-05", capabilities, clientInfo); McpSchema.InitializeRequest request = new McpSchema.InitializeRequest("2024-11-05", capabilities, clientInfo,
meta);
String value = mapper.writeValueAsString(request); String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -316,7 +328,7 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}""")); {"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"},"_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -373,6 +385,7 @@ public class McpSchemaTests {
.mimeType("text/plain") .mimeType("text/plain")
.size(256L) .size(256L)
.annotations(annotations) .annotations(annotations)
.meta(Map.of("metaKey", "metaValue"))
.build(); .build();
String value = mapper.writeValueAsString(resource); String value = mapper.writeValueAsString(resource);
@ -381,7 +394,7 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","size":256,"annotations":{"audience":["user","assistant"],"priority":0.8}}""")); {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","size":256,"annotations":{"audience":["user","assistant"],"priority":0.8},"_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -417,9 +430,10 @@ public class McpSchemaTests {
@Test @Test
void testResourceTemplate() throws Exception { void testResourceTemplate() throws Exception {
McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5); McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5);
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.ResourceTemplate template = new McpSchema.ResourceTemplate("resource://{param}/test", "Test Template", McpSchema.ResourceTemplate template = new McpSchema.ResourceTemplate("resource://{param}/test", "Test Template",
"Test Template", "A test resource template", "text/plain", annotations); "Test Template", "A test resource template", "text/plain", annotations, meta);
String value = mapper.writeValueAsString(template); String value = mapper.writeValueAsString(template);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -427,7 +441,7 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"uriTemplate":"resource://{param}/test","name":"Test Template","title":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}""")); {"uriTemplate":"resource://{param}/test","name":"Test Template","title":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5},"_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -438,8 +452,10 @@ public class McpSchemaTests {
McpSchema.Resource resource2 = new McpSchema.Resource("resource://test2", "Test Resource 2", McpSchema.Resource resource2 = new McpSchema.Resource("resource://test2", "Test Resource 2",
"Second test resource", "application/json", null); "Second test resource", "application/json", null);
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2), McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2),
"next-cursor"); "next-cursor", meta);
String value = mapper.writeValueAsString(result); String value = mapper.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -447,7 +463,7 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); {"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor","_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -472,14 +488,15 @@ public class McpSchemaTests {
@Test @Test
void testReadResourceRequest() throws Exception { void testReadResourceRequest() throws Exception {
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test"); McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test",
Map.of("metaKey", "metaValue"));
String value = mapper.writeValueAsString(request); String value = mapper.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject() .isObject()
.isEqualTo(json(""" .isEqualTo(json("""
{"uri":"resource://test"}""")); {"uri":"resource://test","_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -520,7 +537,8 @@ public class McpSchemaTests {
McpSchema.BlobResourceContents contents2 = new McpSchema.BlobResourceContents("resource://test2", McpSchema.BlobResourceContents contents2 = new McpSchema.BlobResourceContents("resource://test2",
"application/octet-stream", "base64encodedblob"); "application/octet-stream", "base64encodedblob");
McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2)); McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2),
Map.of("metaKey", "metaValue"));
String value = mapper.writeValueAsString(result); String value = mapper.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -528,7 +546,7 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}]}""")); {"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}],"_meta":{"metaKey":"metaValue"}}"""));
} }
// Prompt Tests // Prompt Tests
@ -541,7 +559,7 @@ public class McpSchemaTests {
false); false);
McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "Test Prompt", "A test prompt", McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "Test Prompt", "A test prompt",
Arrays.asList(arg1, arg2)); Arrays.asList(arg1, arg2), Map.of("metaKey", "metaValue"));
String value = mapper.writeValueAsString(prompt); String value = mapper.writeValueAsString(prompt);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -549,7 +567,7 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"name":"test-prompt","title":"Test Prompt","description":"A test prompt","arguments":[{"name":"arg1","title":"First argument","description":"First argument","required":true},{"name":"arg2","title":"Second argument","description":"Second argument","required":false}]}""")); {"name":"test-prompt","title":"Test Prompt","description":"A test prompt","arguments":[{"name":"arg1","title":"First argument","description":"First argument","required":true},{"name":"arg2","title":"Second argument","description":"Second argument","required":false}],"_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -805,6 +823,34 @@ public class McpSchemaTests {
assertThat(deserializedTool.inputSchema().defs()).containsKey("Address"); assertThat(deserializedTool.inputSchema().defs()).containsKey("Address");
} }
@Test
void testToolWithMeta() throws Exception {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
}
},
"required": ["name"]
}
""";
McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.Tool tool = new McpSchema.Tool("addressTool", "addressTool", "Handles addresses", schema, null, null,
meta);
// Verify that meta value was preserved
assertThat(tool.meta()).isNotNull();
assertThat(tool.meta()).containsKey("metaKey");
}
@Test @Test
void testToolWithAnnotations() throws Exception { void testToolWithAnnotations() throws Exception {
String schemaJson = """ String schemaJson = """
@ -1510,14 +1556,14 @@ public class McpSchemaTests {
@Test @Test
void testRoot() throws Exception { void testRoot() throws Exception {
McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root"); McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root", Map.of("metaKey", "metaValue"));
String value = mapper.writeValueAsString(root); String value = mapper.writeValueAsString(root);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject() .isObject()
.isEqualTo(json(""" .isEqualTo(json("""
{"uri":"file:///path/to/root","name":"Test Root"}""")); {"uri":"file:///path/to/root","name":"Test Root","_meta":{"metaKey":"metaValue"}}"""));
} }
@Test @Test
@ -1544,7 +1590,7 @@ public class McpSchemaTests {
@Test @Test
void testProgressNotificationWithMessage() throws Exception { void testProgressNotificationWithMessage() throws Exception {
McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-123", 0.5, 1.0, McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-123", 0.5, 1.0,
"Processing file 1 of 2"); "Processing file 1 of 2", Map.of("key", "value"));
String value = mapper.writeValueAsString(notification); String value = mapper.writeValueAsString(notification);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
@ -1552,19 +1598,21 @@ public class McpSchemaTests {
.isObject() .isObject()
.isEqualTo( .isEqualTo(
json(""" json("""
{"progressToken":"progress-token-123","progress":0.5,"total":1.0,"message":"Processing file 1 of 2"}""")); {"progressToken":"progress-token-123","progress":0.5,"total":1.0,"message":"Processing file 1 of 2","_meta":{"key":"value"}}"""));
} }
@Test @Test
void testProgressNotificationDeserialization() throws Exception { void testProgressNotificationDeserialization() throws Exception {
McpSchema.ProgressNotification notification = mapper.readValue(""" McpSchema.ProgressNotification notification = mapper.readValue(
{"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done"}""", """
{"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done","_meta":{"key":"value"}}""",
McpSchema.ProgressNotification.class); McpSchema.ProgressNotification.class);
assertThat(notification.progressToken()).isEqualTo("token-456"); assertThat(notification.progressToken()).isEqualTo("token-456");
assertThat(notification.progress()).isEqualTo(0.75); assertThat(notification.progress()).isEqualTo(0.75);
assertThat(notification.total()).isEqualTo(1.0); assertThat(notification.total()).isEqualTo(1.0);
assertThat(notification.message()).isEqualTo("Almost done"); assertThat(notification.message()).isEqualTo("Almost done");
assertThat(notification.meta()).containsEntry("key", "value");
} }
@Test @Test