diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 4a570aea..75deb5a0 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -35,6 +35,7 @@ * @author Christian Tzolov * @author Luca Chang * @author Surbhi Bansal + * @author Anurag Pant */ public final class McpSchema { @@ -144,8 +145,9 @@ public static final class ErrorCodes { } - public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest, ElicitRequest, - CompleteRequest, GetPromptRequest, PaginatedRequest, ReadResourceRequest { + public sealed interface Request + permits InitializeRequest, CallToolRequest, CreateMessageRequest, ElicitRequest, CompleteRequest, + GetPromptRequest, ReadResourceRequest, SubscribeRequest, UnsubscribeRequest, PaginatedRequest { Map meta(); @@ -158,6 +160,21 @@ default String progressToken() { } + public sealed interface Result permits InitializeResult, ListResourcesResult, ListResourceTemplatesResult, + ReadResourceResult, ListPromptsResult, GetPromptResult, ListToolsResult, CallToolResult, + CreateMessageResult, ElicitResult, CompleteResult, ListRootsResult { + + Map meta(); + + } + + public sealed interface Notification + permits ProgressNotification, LoggingMessageNotification, ResourcesUpdatedNotification { + + Map meta(); + + } + private static final TypeReference> MAP_TYPE_REF = new TypeReference<>() { }; @@ -211,7 +228,6 @@ public sealed interface JSONRPCMessage permits JSONRPCRequest, JSONRPCNotificati */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) - // TODO: batching support // @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) public record JSONRPCRequest( // @formatter:off @JsonProperty("jsonrpc") String jsonrpc, @@ -312,6 +328,7 @@ public InitializeRequest(String protocolVersion, ClientCapabilities capabilities * This can be used by clients to improve the LLM's understanding of available tools, * resources, etc. It can be thought of like a "hint" to the model. For example, this * information MAY be added to the system prompt + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -319,7 +336,13 @@ public record InitializeResult( // @formatter:off @JsonProperty("protocolVersion") String protocolVersion, @JsonProperty("capabilities") ServerCapabilities capabilities, @JsonProperty("serverInfo") Implementation serverInfo, - @JsonProperty("instructions") String instructions) { // @formatter:on + @JsonProperty("instructions") String instructions, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public InitializeResult(String protocolVersion, ServerCapabilities capabilities, Implementation serverInfo, + String instructions) { + this(protocolVersion, capabilities, serverInfo, instructions, null); + } } /** @@ -660,6 +683,7 @@ public interface BaseMetadata { * sizes and estimate context window usage. * @param annotations Optional annotations for the client. The client can use * annotations to inform how objects are used or displayed. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -670,7 +694,18 @@ public record Resource( // @formatter:off @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, @JsonProperty("size") Long size, - @JsonProperty("annotations") Annotations annotations) implements Annotated, ResourceContent { // @formatter:on + @JsonProperty("annotations") Annotations annotations, + @JsonProperty("_meta") Map meta) implements Annotated, ResourceContent { // @formatter:on + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Resource#builder()} instead. + */ + @Deprecated + public Resource(String uri, String name, String title, String description, String mimeType, Long size, + Annotations annotations) { + this(uri, name, title, description, mimeType, size, annotations, null); + } /** * @deprecated Only exists for backwards-compatibility purposes. Use @@ -679,7 +714,7 @@ public record Resource( // @formatter:off @Deprecated public Resource(String uri, String name, String description, String mimeType, Long size, Annotations annotations) { - this(uri, name, null, description, mimeType, null, annotations); + this(uri, name, null, description, mimeType, size, annotations, null); } /** @@ -688,7 +723,7 @@ public Resource(String uri, String name, String description, String mimeType, Lo */ @Deprecated public Resource(String uri, String name, String description, String mimeType, Annotations annotations) { - this(uri, name, null, description, mimeType, null, annotations); + this(uri, name, null, description, mimeType, null, annotations, null); } public static Builder builder() { @@ -711,6 +746,8 @@ public static class Builder { private Annotations annotations; + private Map meta; + public Builder uri(String uri) { this.uri = uri; return this; @@ -746,11 +783,16 @@ public Builder annotations(Annotations annotations) { return this; } + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + public Resource build() { Assert.hasText(uri, "uri must not be empty"); Assert.hasText(name, "name must not be empty"); - return new Resource(uri, name, title, description, mimeType, size, annotations); + return new Resource(uri, name, title, description, mimeType, size, annotations, meta); } } @@ -758,7 +800,6 @@ public Resource build() { /** * Resource templates allow servers to expose parameterized resources using URI - * templates. * * @param uriTemplate A URI template that can be used to generate URIs for this * resource. @@ -772,6 +813,8 @@ public Resource build() { * @param annotations Optional annotations for the client. The client can use * annotations to inform how objects are used or displayed. * @see RFC 6570 + * @param meta See specification for notes on _meta usage + * */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -781,7 +824,13 @@ public record ResourceTemplate( // @formatter:off @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, - @JsonProperty("annotations") Annotations annotations) implements Annotated, BaseMetadata { // @formatter:on + @JsonProperty("annotations") Annotations annotations, + @JsonProperty("_meta") Map meta) implements Annotated, BaseMetadata { // @formatter:on + + public ResourceTemplate(String uriTemplate, String name, String title, String description, String mimeType, + Annotations annotations) { + this(uriTemplate, name, title, description, mimeType, annotations, null); + } public ResourceTemplate(String uriTemplate, String name, String description, String mimeType, Annotations annotations) { @@ -795,12 +844,18 @@ public ResourceTemplate(String uriTemplate, String name, String description, Str * @param resources A list of resources that the server provides * @param nextCursor An opaque token representing the pagination position after the * last returned result. If present, there may be more results available + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ListResourcesResult( // @formatter:off @JsonProperty("resources") List resources, - @JsonProperty("nextCursor") String nextCursor) { // @formatter:on + @JsonProperty("nextCursor") String nextCursor, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public ListResourcesResult(List resources, String nextCursor) { + this(resources, nextCursor, null); + } } /** @@ -809,12 +864,18 @@ public record ListResourcesResult( // @formatter:off * @param resourceTemplates A list of resource templates that the server provides * @param nextCursor An opaque token representing the pagination position after the * last returned result. If present, there may be more results available + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ListResourceTemplatesResult( // @formatter:off @JsonProperty("resourceTemplates") List resourceTemplates, - @JsonProperty("nextCursor") String nextCursor) { // @formatter:on + @JsonProperty("nextCursor") String nextCursor, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public ListResourceTemplatesResult(List resourceTemplates, String nextCursor) { + this(resourceTemplates, nextCursor, null); + } } /** @@ -839,10 +900,17 @@ public ReadResourceRequest(String uri) { * The server's response to a resources/read request from the client. * * @param contents The contents of the resource + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) - public record ReadResourceResult(@JsonProperty("contents") List contents) { + public record ReadResourceResult( // @formatter:off + @JsonProperty("contents") List contents, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public ReadResourceResult(List contents) { + this(contents, null); + } } /** @@ -851,10 +919,17 @@ public record ReadResourceResult(@JsonProperty("contents") List meta) implements Request { // @formatter:on + + public SubscribeRequest(String uri) { + this(uri, null); + } } /** @@ -862,10 +937,17 @@ public record SubscribeRequest(@JsonProperty("uri") String uri) { * from the server. This should follow a previous resources/subscribe request. * * @param uri The URI of the resource to unsubscribe from + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) - public record UnsubscribeRequest(@JsonProperty("uri") String uri) { + public record UnsubscribeRequest( // @formatter:off + @JsonProperty("uri") String uri, + @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + + public UnsubscribeRequest(String uri) { + this(uri, null); + } } /** @@ -888,6 +970,14 @@ public sealed interface ResourceContents permits TextResourceContents, BlobResou */ String mimeType(); + /** + * @see Specification + * for notes on _meta usage + * @return additional metadata related to this resource. + */ + Map meta(); + } /** @@ -897,13 +987,19 @@ public sealed interface ResourceContents permits TextResourceContents, BlobResou * @param mimeType the MIME type of this resource. * @param text the text of the resource. This must only be set if the resource can * actually be represented as text (not binary data). + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record TextResourceContents( // @formatter:off @JsonProperty("uri") String uri, @JsonProperty("mimeType") String mimeType, - @JsonProperty("text") String text) implements ResourceContents { // @formatter:on + @JsonProperty("text") String text, + @JsonProperty("_meta") Map meta) implements ResourceContents { // @formatter:on + + public TextResourceContents(String uri, String mimeType, String text) { + this(uri, mimeType, text, null); + } } /** @@ -914,13 +1010,19 @@ public record TextResourceContents( // @formatter:off * @param blob a base64-encoded string representing the binary data of the resource. * This must only be set if the resource can actually be represented as binary data * (not text). + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record BlobResourceContents( // @formatter:off @JsonProperty("uri") String uri, @JsonProperty("mimeType") String mimeType, - @JsonProperty("blob") String blob) implements ResourceContents { // @formatter:on + @JsonProperty("blob") String blob, + @JsonProperty("_meta") Map meta) implements ResourceContents { // @formatter:on + + public BlobResourceContents(String uri, String mimeType, String blob) { + this(uri, mimeType, blob, null); + } } // --------------------------- @@ -933,6 +1035,7 @@ public record BlobResourceContents( // @formatter:off * @param title An optional title for the prompt. * @param description An optional description of what this prompt provides. * @param arguments A list of arguments to use for templating the prompt. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -940,11 +1043,16 @@ public record Prompt( // @formatter:off @JsonProperty("name") String name, @JsonProperty("title") String title, @JsonProperty("description") String description, - @JsonProperty("arguments") List arguments) implements BaseMetadata { // @formatter:on + @JsonProperty("arguments") List arguments, + @JsonProperty("_meta") Map meta) implements BaseMetadata { // @formatter:on public Prompt(String name, String description, List arguments) { this(name, null, description, arguments != null ? arguments : new ArrayList<>()); } + + public Prompt(String name, String title, String description, List arguments) { + this(name, title, description, arguments != null ? arguments : new ArrayList<>(), null); + } } /** @@ -990,12 +1098,18 @@ public record PromptMessage( // @formatter:off * @param prompts A list of prompts that the server provides. * @param nextCursor An optional cursor for pagination. If present, indicates there * are more prompts available. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ListPromptsResult( // @formatter:off @JsonProperty("prompts") List prompts, - @JsonProperty("nextCursor") String nextCursor) { // @formatter:on + @JsonProperty("nextCursor") String nextCursor, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public ListPromptsResult(List prompts, String nextCursor) { + this(prompts, nextCursor, null); + } } /** @@ -1003,6 +1117,7 @@ public record ListPromptsResult( // @formatter:off * * @param name The name of the prompt or prompt template. * @param arguments Arguments to use for templating the prompt. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1021,12 +1136,18 @@ public GetPromptRequest(String name, Map arguments) { * * @param description An optional description for the prompt. * @param messages A list of messages to display as part of the prompt. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record GetPromptResult( // @formatter:off @JsonProperty("description") String description, - @JsonProperty("messages") List messages) { // @formatter:on + @JsonProperty("messages") List messages, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public GetPromptResult(String description, List messages) { + this(description, messages, null); + } } // --------------------------- @@ -1038,12 +1159,18 @@ public record GetPromptResult( // @formatter:off * @param tools A list of tools that the server provides. * @param nextCursor An optional cursor for pagination. If present, indicates there * are more tools available. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ListToolsResult( // @formatter:off @JsonProperty("tools") List tools, - @JsonProperty("nextCursor") String nextCursor) { // @formatter:on + @JsonProperty("nextCursor") String nextCursor, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + + public ListToolsResult(List tools, String nextCursor) { + this(tools, nextCursor, null); + } } /** @@ -1103,6 +1230,7 @@ public record ToolAnnotations( // @formatter:off * @param outputSchema An optional JSON Schema object defining the structure of the * tool's output returned in the structuredContent field of a CallToolResult. * @param annotations Optional additional tool information. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1112,7 +1240,8 @@ public record Tool( // @formatter:off @JsonProperty("description") String description, @JsonProperty("inputSchema") JsonSchema inputSchema, @JsonProperty("outputSchema") Map outputSchema, - @JsonProperty("annotations") ToolAnnotations annotations) { // @formatter:on + @JsonProperty("annotations") ToolAnnotations annotations, + @JsonProperty("_meta") Map meta) { // @formatter:on /** * @deprecated Only exists for backwards-compatibility purposes. Use @@ -1120,7 +1249,7 @@ public record Tool( // @formatter:off */ @Deprecated public Tool(String name, String description, JsonSchema inputSchema, ToolAnnotations annotations) { - this(name, null, description, inputSchema, null, annotations); + this(name, null, description, inputSchema, null, annotations, null); } /** @@ -1129,7 +1258,7 @@ public Tool(String name, String description, JsonSchema inputSchema, ToolAnnotat */ @Deprecated public Tool(String name, String description, String inputSchema) { - this(name, null, description, parseSchema(inputSchema), null, null); + this(name, null, description, parseSchema(inputSchema), null, null, null); } /** @@ -1138,7 +1267,7 @@ public Tool(String name, String description, String inputSchema) { */ @Deprecated public Tool(String name, String description, String schema, ToolAnnotations annotations) { - this(name, null, description, parseSchema(schema), null, annotations); + this(name, null, description, parseSchema(schema), null, annotations, null); } /** @@ -1148,7 +1277,17 @@ public Tool(String name, String description, String schema, ToolAnnotations anno @Deprecated public Tool(String name, String description, String inputSchema, String outputSchema, ToolAnnotations annotations) { - this(name, null, description, parseSchema(inputSchema), schemaToMap(outputSchema), annotations); + this(name, null, description, parseSchema(inputSchema), schemaToMap(outputSchema), annotations, null); + } + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Tool#builder()} instead. + */ + @Deprecated + public Tool(String name, String title, String description, String inputSchema, String outputSchema, + ToolAnnotations annotations) { + this(name, title, description, parseSchema(inputSchema), schemaToMap(outputSchema), annotations, null); } public static Builder builder() { @@ -1169,6 +1308,8 @@ public static class Builder { private ToolAnnotations annotations; + private Map meta; + public Builder name(String name) { this.name = name; return this; @@ -1209,9 +1350,14 @@ public Builder annotations(ToolAnnotations annotations) { return this; } + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + public Tool build() { Assert.hasText(name, "name must not be empty"); - return new Tool(name, title, description, inputSchema, outputSchema, annotations); + return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta); } } @@ -1242,6 +1388,7 @@ private static JsonSchema parseSchema(String schema) { * tools/list. * @param arguments Arguments to pass to the tool. These must conform to the tool's * input schema. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1324,17 +1471,24 @@ public CallToolRequest build() { * contains error information. If false or absent, indicates successful execution. * @param structuredContent An optional JSON object that represents the structured * result of the tool call. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record CallToolResult( // @formatter:off @JsonProperty("content") List content, @JsonProperty("isError") Boolean isError, - @JsonProperty("structuredContent") Map structuredContent) { // @formatter:on + @JsonProperty("structuredContent") Map structuredContent, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on // backwards compatibility constructor public CallToolResult(List content, Boolean isError) { - this(content, isError, null); + this(content, isError, null, null); + } + + // backwards compatibility constructor + public CallToolResult(List content, Boolean isError, Map structuredContent) { + this(content, isError, structuredContent, null); } /** @@ -1347,7 +1501,7 @@ public CallToolResult(List content, Boolean isError) { * execution. */ public CallToolResult(String content, Boolean isError) { - this(List.of(new TextContent(content)), isError); + this(List.of(new TextContent(content)), isError, null); } /** @@ -1369,6 +1523,8 @@ public static class Builder { private Map structuredContent; + private Map meta; + /** * Sets the content list for the tool result. * @param content the content list @@ -1443,12 +1599,22 @@ public Builder isError(Boolean isError) { return this; } + /** + * Sets the metadata for the tool result. + * @param meta metadata + * @return this builder + */ + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + /** * Builds a new {@link CallToolResult} instance. * @return a new CallToolResult instance */ public CallToolResult build() { - return new CallToolResult(content, isError, structuredContent); + return new CallToolResult(content, isError, structuredContent, meta); } } @@ -1710,6 +1876,7 @@ public CreateMessageRequest build() { * @param content The content of the sampled message * @param model The name of the model that generated the message * @param stopReason The reason why sampling stopped, if known + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1717,7 +1884,8 @@ public record CreateMessageResult( // @formatter:off @JsonProperty("role") Role role, @JsonProperty("content") Content content, @JsonProperty("model") String model, - @JsonProperty("stopReason") StopReason stopReason) { // @formatter:on + @JsonProperty("stopReason") StopReason stopReason, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on public enum StopReason { @@ -1744,6 +1912,10 @@ private static StopReason of(String value) { } + public CreateMessageResult(Role role, Content content, String model, StopReason stopReason) { + this(role, content, model, stopReason, null); + } + public static Builder builder() { return new Builder(); } @@ -1758,6 +1930,8 @@ public static class Builder { private StopReason stopReason = StopReason.END_TURN; + private Map meta; + public Builder role(Role role) { this.role = role; return this; @@ -1783,8 +1957,13 @@ public Builder message(String message) { return this; } + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + public CreateMessageResult build() { - return new CreateMessageResult(role, content, model, stopReason); + return new CreateMessageResult(role, content, model, stopReason, meta); } } @@ -1862,12 +2041,14 @@ public ElicitRequest build() { * action, "cancel": User dismissed without making an explicit choice * @param content The submitted form data, only present when action is "accept". * Contains values matching the requested schema + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ElicitResult( // @formatter:off @JsonProperty("action") Action action, - @JsonProperty("content") Map content) { // @formatter:on + @JsonProperty("content") Map content, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on public enum Action { @@ -1877,6 +2058,11 @@ public enum Action { @JsonProperty("cancel") CANCEL } // @formatter:on + // backwards compatibility constructor + public ElicitResult(Action action, Map content) { + this(action, content, null); + } + public static Builder builder() { return new Builder(); } @@ -1887,6 +2073,8 @@ public static class Builder { private Map content; + private Map meta; + public Builder message(Action action) { this.action = action; return this; @@ -1897,8 +2085,13 @@ public Builder content(Map content) { return this; } + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + public ElicitResult build() { - return new ElicitResult(action, content); + return new ElicitResult(action, content, meta); } } @@ -1957,6 +2150,7 @@ public record PaginatedResult(@JsonProperty("nextCursor") String nextCursor) { * @param progress A value indicating the current progress. * @param total An optional total amount of work to be done, if known. * @param message An optional message providing additional context about the progress. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1964,7 +2158,12 @@ public record ProgressNotification( // @formatter:off @JsonProperty("progressToken") String progressToken, @JsonProperty("progress") Double progress, @JsonProperty("total") Double total, - @JsonProperty("message") String message) { // @formatter:on + @JsonProperty("message") String message, + @JsonProperty("_meta") Map meta) implements Notification { // @formatter:on + + public ProgressNotification(String progressToken, double progress, Double total, String message) { + this(progressToken, progress, total, message, null); + } } /** @@ -1972,9 +2171,16 @@ public record ProgressNotification( // @formatter:off * resources update message to clients. * * @param uri The updated resource uri. + * @param meta See specification for notes on _meta usage */ @JsonIgnoreProperties(ignoreUnknown = true) - public record ResourcesUpdatedNotification(@JsonProperty("uri") String uri) { + public record ResourcesUpdatedNotification(// @formatter:off + @JsonProperty("uri") String uri, + @JsonProperty("_meta") Map meta) implements Notification { // @formatter:on + + public ResourcesUpdatedNotification(String uri) { + this(uri, null); + } } /** @@ -1986,12 +2192,19 @@ public record ResourcesUpdatedNotification(@JsonProperty("uri") String uri) { * @param level The severity levels. The minimum log level is set by the client. * @param logger The logger that generated the message. * @param data JSON-serializable logging data. + * @param meta See specification for notes on _meta usage */ @JsonIgnoreProperties(ignoreUnknown = true) public record LoggingMessageNotification( // @formatter:off @JsonProperty("level") LoggingLevel level, @JsonProperty("logger") String logger, - @JsonProperty("data") String data) { // @formatter:on + @JsonProperty("data") String data, + @JsonProperty("_meta") Map meta) implements Notification { // @formatter:on + + // backwards compatibility constructor + public LoggingMessageNotification(LoggingLevel level, String logger, String data) { + this(level, logger, data, null); + } public static Builder builder() { return new Builder(); @@ -2005,6 +2218,8 @@ public static class Builder { private String data; + private Map meta; + public Builder level(LoggingLevel level) { this.level = level; return this; @@ -2020,8 +2235,13 @@ public Builder data(String data) { return this; } + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + public LoggingMessageNotification build() { - return new LoggingMessageNotification(level, logger, data); + return new LoggingMessageNotification(level, logger, data, meta); } } @@ -2175,10 +2395,17 @@ public record CompleteContext(@JsonProperty("arguments") Map arg * The server's response to a completion/complete request. * * @param completion The completion information containing values and metadata + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) - public record CompleteResult(@JsonProperty("completion") CompleteCompletion completion) { + public record CompleteResult(@JsonProperty("completion") CompleteCompletion completion, + @JsonProperty("_meta") Map meta) implements Result { + + // backwards compatibility constructor + public CompleteResult(CompleteCompletion completion) { + this(completion, null); + } /** * The server's response to a completion/complete request @@ -2207,6 +2434,8 @@ public record CompleteCompletion( // @formatter:off @JsonSubTypes.Type(value = ResourceLink.class, name = "resource_link") }) public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource, ResourceLink { + Map meta(); + default String type() { if (this instanceof TextContent) { return "text"; @@ -2233,29 +2462,37 @@ else if (this instanceof ResourceLink) { * * @param annotations Optional annotations for the client * @param text The text content of the message + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record TextContent( // @formatter:off @JsonProperty("annotations") Annotations annotations, - @JsonProperty("text") String text) implements Annotated, Content { // @formatter:on + @JsonProperty("text") String text, + @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + + public TextContent(Annotations annotations, String text) { + this(annotations, text, null); + } public TextContent(String content) { - this(null, content); + this(null, content, null); } /** * @deprecated Only exists for backwards-compatibility purposes. Use * {@link TextContent#TextContent(Annotations, String)} instead. */ + @Deprecated public TextContent(List audience, Double priority, String content) { - this(audience != null || priority != null ? new Annotations(audience, priority) : null, content); + this(audience != null || priority != null ? new Annotations(audience, priority) : null, content, null); } /** * @deprecated Only exists for backwards-compatibility purposes. Use * {@link TextContent#annotations()} instead. */ + @Deprecated public List audience() { return annotations == null ? null : annotations.audience(); } @@ -2264,6 +2501,7 @@ public List audience() { * @deprecated Only exists for backwards-compatibility purposes. Use * {@link TextContent#annotations()} instead. */ + @Deprecated public Double priority() { return annotations == null ? null : annotations.priority(); } @@ -2276,26 +2514,35 @@ public Double priority() { * @param data The base64-encoded image data * @param mimeType The MIME type of the image. Different providers may support * different image types + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ImageContent( // @formatter:off @JsonProperty("annotations") Annotations annotations, @JsonProperty("data") String data, - @JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on + @JsonProperty("mimeType") String mimeType, + @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + + public ImageContent(Annotations annotations, String data, String mimeType) { + this(annotations, data, mimeType, null); + } /** * @deprecated Only exists for backwards-compatibility purposes. Use * {@link ImageContent#ImageContent(Annotations, String, String)} instead. */ + @Deprecated public ImageContent(List audience, Double priority, String data, String mimeType) { - this(audience != null || priority != null ? new Annotations(audience, priority) : null, data, mimeType); + this(audience != null || priority != null ? new Annotations(audience, priority) : null, data, mimeType, + null); } /** * @deprecated Only exists for backwards-compatibility purposes. Use * {@link ImageContent#annotations()} instead. */ + @Deprecated public List audience() { return annotations == null ? null : annotations.audience(); } @@ -2304,6 +2551,7 @@ public List audience() { * @deprecated Only exists for backwards-compatibility purposes. Use * {@link ImageContent#annotations()} instead. */ + @Deprecated public Double priority() { return annotations == null ? null : annotations.priority(); } @@ -2316,13 +2564,20 @@ public Double priority() { * @param data The base64-encoded audio data * @param mimeType The MIME type of the audio. Different providers may support * different audio types + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record AudioContent( // @formatter:off @JsonProperty("annotations") Annotations annotations, @JsonProperty("data") String data, - @JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on + @JsonProperty("mimeType") String mimeType, + @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + + // backwards compatibility constructor + public AudioContent(Annotations annotations, String data, String mimeType) { + this(annotations, data, mimeType, null); + } } /** @@ -2333,26 +2588,35 @@ public record AudioContent( // @formatter:off * * @param annotations Optional annotations for the client * @param resource The resource contents that are embedded + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record EmbeddedResource( // @formatter:off @JsonProperty("annotations") Annotations annotations, - @JsonProperty("resource") ResourceContents resource) implements Annotated, Content { // @formatter:on + @JsonProperty("resource") ResourceContents resource, + @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + + // backwards compatibility constructor + public EmbeddedResource(Annotations annotations, ResourceContents resource) { + this(annotations, resource, null); + } /** * @deprecated Only exists for backwards-compatibility purposes. Use * {@link EmbeddedResource#EmbeddedResource(Annotations, ResourceContents)} * instead. */ + @Deprecated public EmbeddedResource(List audience, Double priority, ResourceContents resource) { - this(audience != null || priority != null ? new Annotations(audience, priority) : null, resource); + this(audience != null || priority != null ? new Annotations(audience, priority) : null, resource, null); } /** * @deprecated Only exists for backwards-compatibility purposes. Use * {@link EmbeddedResource#annotations()} instead. */ + @Deprecated public List audience() { return annotations == null ? null : annotations.audience(); } @@ -2361,6 +2625,7 @@ public List audience() { * @deprecated Only exists for backwards-compatibility purposes. Use * {@link EmbeddedResource#annotations()} instead. */ + @Deprecated public Double priority() { return annotations == null ? null : annotations.priority(); } @@ -2382,6 +2647,7 @@ public Double priority() { * sizes and estimate context window usage. * @param annotations Optional annotations for the client. The client can use * annotations to inform how objects are used or displayed. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -2392,7 +2658,19 @@ public record ResourceLink( // @formatter:off @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, @JsonProperty("size") Long size, - @JsonProperty("annotations") Annotations annotations) implements Annotated, Content, ResourceContent { // @formatter:on + @JsonProperty("annotations") Annotations annotations, + @JsonProperty("_meta") Map meta) implements Annotated, Content, ResourceContent { // @formatter:on + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link ResourceLink#ResourceLink(String, String, String, String, String, Long, Annotations)} + * instead. + */ + @Deprecated + public ResourceLink(String name, String title, String uri, String description, String mimeType, Long size, + Annotations annotations) { + this(name, title, uri, description, mimeType, size, annotations, null); + } /** * @deprecated Only exists for backwards-compatibility purposes. Use @@ -2425,6 +2703,8 @@ public static class Builder { private Long size; + private Map meta; + public Builder name(String name) { this.name = name; return this; @@ -2460,11 +2740,16 @@ public Builder size(Long size) { return this; } + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + public ResourceLink build() { Assert.hasText(uri, "uri must not be empty"); Assert.hasText(name, "name must not be empty"); - return new ResourceLink(name, title, uri, description, mimeType, size, annotations); + return new ResourceLink(name, title, uri, description, mimeType, size, annotations, meta); } } @@ -2482,12 +2767,18 @@ public ResourceLink build() { * @param name An optional name for the root. This can be used to provide a * human-readable identifier for the root, which may be useful for display purposes or * for referencing the root in other parts of the application. + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record Root( // @formatter:off @JsonProperty("uri") String uri, - @JsonProperty("name") String name) { // @formatter:on + @JsonProperty("name") String name, + @JsonProperty("_meta") Map meta) { // @formatter:on + + public Root(String uri, String name) { + this(uri, name, null); + } } /** @@ -2500,16 +2791,22 @@ public record Root( // @formatter:off * @param nextCursor An optional cursor for pagination. If present, indicates there * are more roots available. The client can use this cursor to request the next page * of results by sending a roots/list request with the cursor parameter set to this + * @param meta See specification for notes on _meta usage */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record ListRootsResult( // @formatter:off @JsonProperty("roots") List roots, - @JsonProperty("nextCursor") String nextCursor) { // @formatter:on + @JsonProperty("nextCursor") String nextCursor, + @JsonProperty("_meta") Map meta) implements Result { // @formatter:on public ListRootsResult(List roots) { this(roots, null); } + + public ListRootsResult(List roots, String nextCursor) { + this(roots, nextCursor, null); + } } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index df7ab514..fbbb4307 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -24,6 +24,7 @@ /** * @author Christian Tzolov + * @author Anurag Pant */ public class McpSchemaTests { @@ -46,11 +47,12 @@ void testTextContent() throws Exception { @Test void testTextContentDeserialization() throws Exception { 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.type()).isEqualTo("text"); assertThat(textContent.text()).isEqualTo("XXX"); + assertThat(textContent.meta()).containsKey("metaKey"); } @Test @@ -78,11 +80,13 @@ void testImageContent() throws Exception { @Test void testImageContentDeserialization() throws Exception { 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.type()).isEqualTo("image"); assertThat(imageContent.data()).isEqualTo("base64encodeddata"); assertThat(imageContent.mimeType()).isEqualTo("image/png"); + assertThat(imageContent.meta()).containsKey("metaKey"); } @Test @@ -100,11 +104,13 @@ void testAudioContent() throws Exception { @Test void testAudioContentDeserialization() throws Exception { 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.type()).isEqualTo("audio"); assertThat(audioContent.data()).isEqualTo("base64encodeddata"); assertThat(audioContent.mimeType()).isEqualTo("audio/wav"); + assertThat(audioContent.meta()).containsKey("metaKey"); } @Test @@ -164,7 +170,7 @@ void testEmbeddedResource() throws Exception { void testEmbeddedResourceDeserialization() throws Exception { 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); assertThat(embeddedResource).isNotNull(); assertThat(embeddedResource.type()).isEqualTo("resource"); @@ -172,6 +178,7 @@ void testEmbeddedResourceDeserialization() throws Exception { assertThat(embeddedResource.resource().uri()).isEqualTo("resource://test"); assertThat(embeddedResource.resource().mimeType()).isEqualTo("text/plain"); assertThat(((TextResourceContents) embeddedResource.resource()).text()).isEqualTo("Sample resource content"); + assertThat(embeddedResource.meta()).containsKey("metaKey"); } @Test @@ -194,7 +201,7 @@ void testEmbeddedResourceWithBlobContents() throws Exception { void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { 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); assertThat(embeddedResource).isNotNull(); assertThat(embeddedResource.type()).isEqualTo("resource"); @@ -203,12 +210,14 @@ void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { assertThat(embeddedResource.resource().mimeType()).isEqualTo("application/octet-stream"); assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).blob()) .isEqualTo("base64encodedblob"); + assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).meta()).containsKey("metaKey"); } @Test void testResourceLink() throws Exception { 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -216,14 +225,14 @@ void testResourceLink() throws Exception { .isObject() .isEqualTo( 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 void testResourceLinkDeserialization() throws Exception { 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); assertThat(resourceLink).isNotNull(); assertThat(resourceLink.type()).isEqualTo("resource_link"); @@ -231,6 +240,7 @@ void testResourceLinkDeserialization() throws Exception { assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs"); assertThat(resourceLink.description()).isEqualTo("Primary application entry point"); assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust"); + assertThat(resourceLink.meta()).containsEntry("metaKey", "metaValue"); } // JSON-RPC Message Types Tests @@ -307,8 +317,10 @@ void testInitializeRequest() throws Exception { .build(); McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); + Map 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -316,7 +328,7 @@ void testInitializeRequest() throws Exception { .isObject() .isEqualTo( 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 @@ -373,6 +385,7 @@ void testResourceBuilder() throws Exception { .mimeType("text/plain") .size(256L) .annotations(annotations) + .meta(Map.of("metaKey", "metaValue")) .build(); String value = mapper.writeValueAsString(resource); @@ -381,7 +394,7 @@ void testResourceBuilder() throws Exception { .isObject() .isEqualTo( 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 @@ -417,9 +430,10 @@ void testResourceBuilderNameRequired() { @Test void testResourceTemplate() throws Exception { McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5); + Map meta = Map.of("metaKey", "metaValue"); 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -427,7 +441,7 @@ void testResourceTemplate() throws Exception { .isObject() .isEqualTo( 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 @@ -438,8 +452,10 @@ void testListResourcesResult() throws Exception { McpSchema.Resource resource2 = new McpSchema.Resource("resource://test2", "Test Resource 2", "Second test resource", "application/json", null); + Map meta = Map.of("metaKey", "metaValue"); + McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2), - "next-cursor"); + "next-cursor", meta); String value = mapper.writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -447,7 +463,7 @@ void testListResourcesResult() throws Exception { .isObject() .isEqualTo( 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 @@ -472,14 +488,15 @@ void testListResourceTemplatesResult() throws Exception { @Test 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() .isEqualTo(json(""" - {"uri":"resource://test"}""")); + {"uri":"resource://test","_meta":{"metaKey":"metaValue"}}""")); } @Test @@ -520,7 +537,8 @@ void testReadResourceResult() throws Exception { McpSchema.BlobResourceContents contents2 = new McpSchema.BlobResourceContents("resource://test2", "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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -528,7 +546,7 @@ void testReadResourceResult() throws Exception { .isObject() .isEqualTo( 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 @@ -541,7 +559,7 @@ void testPrompt() throws Exception { false); 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -549,7 +567,7 @@ void testPrompt() throws Exception { .isObject() .isEqualTo( 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 @@ -805,6 +823,34 @@ void testToolWithComplexSchema() throws Exception { 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 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 void testToolWithAnnotations() throws Exception { String schemaJson = """ @@ -1510,14 +1556,14 @@ void testCompleteRequestWithMeta() throws Exception { @Test 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() .isEqualTo(json(""" - {"uri":"file:///path/to/root","name":"Test Root"}""")); + {"uri":"file:///path/to/root","name":"Test Root","_meta":{"metaKey":"metaValue"}}""")); } @Test @@ -1544,7 +1590,7 @@ void testListRootsResult() throws Exception { @Test void testProgressNotificationWithMessage() throws Exception { 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); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -1552,19 +1598,21 @@ void testProgressNotificationWithMessage() throws Exception { .isObject() .isEqualTo( 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 void testProgressNotificationDeserialization() throws Exception { - McpSchema.ProgressNotification notification = mapper.readValue(""" - {"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done"}""", + McpSchema.ProgressNotification notification = mapper.readValue( + """ + {"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done","_meta":{"key":"value"}}""", McpSchema.ProgressNotification.class); assertThat(notification.progressToken()).isEqualTo("token-456"); assertThat(notification.progress()).isEqualTo(0.75); assertThat(notification.total()).isEqualTo(1.0); assertThat(notification.message()).isEqualTo("Almost done"); + assertThat(notification.meta()).containsEntry("key", "value"); } @Test