From fb873652ba58b722f91b3aefabb54970ab1c78e6 Mon Sep 17 00:00:00 2001 From: YunKui Lu Date: Sat, 28 Jun 2025 18:17:00 +0800 Subject: [PATCH 1/5] feat: Added optional title field to `resource`, `tool`, `prompt` Signed-off-by: YunKui Lu --- .../modelcontextprotocol/spec/McpSchema.java | 321 +++++++++++++++++- .../McpAsyncClientResponseHandlerTests.java | 11 +- .../server/AbstractMcpAsyncServerTests.java | 21 +- .../server/AbstractMcpSyncServerTests.java | 22 +- .../spec/McpSchemaTests.java | 32 +- 5 files changed, 382 insertions(+), 25 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index f8eb99e3a..4fe7f2ad5 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -324,6 +324,9 @@ public static class Builder { private Sampling sampling; private Elicitation elicitation; + private Builder(){ + } + public Builder experimental(Map experimental) { this.experimental = experimental; return this; @@ -397,6 +400,9 @@ public static class Builder { private ResourceCapabilities resources; private ToolCapabilities tools; + private Builder(){ + } + public Builder completions() { this.completions = new CompletionCapabilities(); return this; @@ -483,8 +489,12 @@ public record Annotations( // @formatter:off * A known resource that the server is capable of reading. * * @param uri the URI of the resource. - * @param name A human-readable name for this resource. This can be used by clients to - * populate UI elements. + * @param name Intended for programmatic or logical use, but used as a display name in + * past specs or fallback (if title isn't present). + * @param title Intended for UI and end-user contexts — optimized to be human-readable + * and easily understood, even by those unfamiliar with domain-specific terminology. + * If not provided, the name should be used for display (where annotations.title + * should be given precedence over using the name). * @param description A description of what this resource represents. This can be used * by clients to improve the LLM's understanding of available resources. It can be * thought of like a "hint" to the model. @@ -500,6 +510,7 @@ public record Annotations( // @formatter:off public record Resource( // @formatter:off @JsonProperty("uri") String uri, @JsonProperty("name") String name, + @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, @JsonProperty("size") Long size, @@ -511,7 +522,7 @@ public record Resource( // @formatter:off */ @Deprecated public Resource(String uri, String name, String description, String mimeType, Annotations annotations) { - this(uri, name, description, mimeType, null, annotations); + this(uri, name, null, description, mimeType, null, annotations); } public static Builder builder() { @@ -521,11 +532,15 @@ public static Builder builder() { public static class Builder { private String uri; private String name; + private String title; private String description; private String mimeType; private Long size; private Annotations annotations; + private Builder(){ + } + public Builder uri(String uri) { this.uri = uri; return this; @@ -536,6 +551,11 @@ public Builder name(String name) { return this; } + public Builder title(String title) { + this.title = title; + return this; + } + public Builder description(String description) { this.description = description; return this; @@ -560,7 +580,7 @@ public Resource build() { Assert.hasText(uri, "uri must not be empty"); Assert.hasText(name, "name must not be empty"); - return new Resource(uri, name, description, mimeType, size, annotations); + return new Resource(uri, name, title, description, mimeType, size, annotations); } } } // @formatter:on @@ -573,6 +593,10 @@ public Resource build() { * resource. * @param name A human-readable name for this resource. This can be used by clients to * populate UI elements. + * @param title Intended for UI and end-user contexts — optimized to be human-readable + * and easily understood, even by those unfamiliar with domain-specific terminology. + * If not provided, the name should be used for display (where annotations.title + * should be given precedence over using the name). * @param description A description of what this resource represents. This can be used * by clients to improve the LLM's understanding of available resources. It can be * thought of like a "hint" to the model. @@ -586,9 +610,69 @@ public Resource build() { public record ResourceTemplate( // @formatter:off @JsonProperty("uriTemplate") String uriTemplate, @JsonProperty("name") String name, + @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, @JsonProperty("annotations") Annotations annotations) implements Annotated { + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link ResourceTemplate#builder()} instead. + */ + @Deprecated + public ResourceTemplate(String uriTemplate, String name, String description, String mimeType, Annotations annotations) { + this(uriTemplate, name, null, description, mimeType, annotations); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String uriTemplate; + private String name; + private String title; + private String description; + private String mimeType; + private Annotations annotations; + + private Builder(){ + } + + public Builder uriTemplate(String uriTemplate) { + this.uriTemplate = uriTemplate; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder mimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + public Builder annotations(Annotations annotations) { + this.annotations = annotations; + return this; + } + + public ResourceTemplate build() { + return new ResourceTemplate(uriTemplate, name, title, description, mimeType, annotations); + } + } } // @formatter:on @JsonInclude(JsonInclude.Include.NON_ABSENT) @@ -698,6 +782,10 @@ public record BlobResourceContents( // @formatter:off * A prompt or prompt template that the server offers. * * @param name The name of the prompt or prompt template. + * @param title Intended for UI and end-user contexts — optimized to be human-readable + * and easily understood, even by those unfamiliar with domain-specific terminology. + * If not provided, the name should be used for display (where annotations.title + * should be given precedence over using the name). * @param description An optional description of what this prompt provides. * @param arguments A list of arguments to use for templating the prompt. */ @@ -705,8 +793,55 @@ public record BlobResourceContents( // @formatter:off @JsonIgnoreProperties(ignoreUnknown = true) public record Prompt( // @formatter:off @JsonProperty("name") String name, + @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("arguments") List arguments) { + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Prompt#builder()} instead. + */ + @Deprecated + public Prompt(String name, String description, List arguments) { + this(name, null, description, arguments); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String title; + private String description; + private List arguments; + + private Builder(){ + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder arguments(List arguments) { + this.arguments = arguments; + return this; + } + public Prompt build() { + return new Prompt(name, title, description, arguments); + } + } } // @formatter:on /** @@ -720,8 +855,56 @@ public record Prompt( // @formatter:off @JsonIgnoreProperties(ignoreUnknown = true) public record PromptArgument( // @formatter:off @JsonProperty("name") String name, + @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("required") Boolean required) { + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link PromptArgument#builder()} instead. + */ + @Deprecated + public PromptArgument(String name, String description, Boolean required) { + this(name, null, description, required); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String title; + private String description; + private Boolean required; + + private Builder(){ + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder required(Boolean required) { + this.required = required; + return this; + } + + public PromptArgument build() { + return new PromptArgument(name, title, description, required); + } + } }// @formatter:on /** @@ -826,6 +1009,10 @@ public record ToolAnnotations( // @formatter:off * * @param name A unique identifier for the tool. This name is used when calling the * tool. + * @param title Intended for UI and end-user contexts — optimized to be human-readable + * and easily understood, even by those unfamiliar with domain-specific terminology. + * If not provided, the name should be used for display (except for tools, where + * annotations.title should be given precedence over using the name). * @param description A human-readable description of what the tool does. This can be * used by clients to improve the LLM's understanding of available tools. * @param inputSchema A JSON Schema object that describes the expected structure of @@ -837,18 +1024,72 @@ public record ToolAnnotations( // @formatter:off @JsonIgnoreProperties(ignoreUnknown = true) public record Tool( // @formatter:off @JsonProperty("name") String name, + @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("inputSchema") JsonSchema inputSchema, @JsonProperty("annotations") ToolAnnotations annotations) { + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Tool#builder()} instead. + */ + @Deprecated public Tool(String name, String description, String schema) { - this(name, description, parseSchema(schema), null); + this(name, null, description, parseSchema(schema), null); } + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Tool#builder()} instead. + */ + @Deprecated public Tool(String name, String description, String schema, ToolAnnotations annotations) { - this(name, description, parseSchema(schema), annotations); + this(name, null, description, parseSchema(schema), annotations); } + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String title; + private String description; + private JsonSchema inputSchema; + private ToolAnnotations annotations; + + private Builder(){ + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder inputSchema(JsonSchema inputSchema) { + this.inputSchema = inputSchema; + return this; + } + + public Builder annotations(ToolAnnotations annotations) { + this.annotations = annotations; + return this; + } + + public Tool build() { + return new Tool(name, title, description, inputSchema, annotations); + } + } } // @formatter:on private static JsonSchema parseSchema(String schema) { @@ -930,6 +1171,9 @@ public static class Builder { private List content = new ArrayList<>(); private Boolean isError; + private Builder(){ + } + /** * Sets the content list for the tool result. * @param content the content list @@ -1021,6 +1265,9 @@ public static class Builder { private Double speedPriority; private Double intelligencePriority; + private Builder(){ + } + public Builder hints(List hints) { this.hints = hints; return this; @@ -1103,6 +1350,9 @@ public static class Builder { private List stopSequences; private Map metadata; + private Builder(){ + } + public Builder messages(List messages) { this.messages = messages; return this; @@ -1189,6 +1439,9 @@ public static class Builder { private String model; private StopReason stopReason = StopReason.END_TURN; + private Builder(){ + } + public Builder role(Role role) { this.role = role; return this; @@ -1241,6 +1494,9 @@ public static class Builder { private String message; private Map requestedSchema; + private Builder(){ + } + public Builder message(String message) { this.message = message; return this; @@ -1277,6 +1533,9 @@ public static class Builder { private Action action; private Map content; + private Builder(){ + } + public Builder message(Action action) { this.action = action; return this; @@ -1352,6 +1611,9 @@ public static class Builder { private String logger = "server"; private String data; + private Builder(){ + } + public Builder level(LoggingLevel level) { this.level = level; return this; @@ -1613,12 +1875,57 @@ public Double priority() { * @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 title Intended for UI and end-user contexts — optimized to be human-readable + * and easily understood, even by those unfamiliar with domain-specific terminology. + * If not provided, the name should be used for display (where annotations.title + * should be given precedence over using the name). */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record Root( // @formatter:off @JsonProperty("uri") String uri, - @JsonProperty("name") String name) { + @JsonProperty("name") String name, + @JsonProperty("title") String title) { + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Root#builder()} instead. + */ + @Deprecated + public Root(String uri, String name){ + this(uri, name, null); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String uri; + private String name; + private String title; + + private Builder() { + } + + public Builder uri(String uri) { + this.uri = uri; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + public Root build() { + return new Root(uri, name, title); + } + } } // @formatter:on /** diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index a79bdf6c9..21d40aa7e 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -207,8 +207,14 @@ void testResourcesChangeNotificationHandling() { assertThat(asyncMcpClient.initialize().block()).isNotNull(); // Create a mock resources list that the server will return - McpSchema.Resource mockResource = new McpSchema.Resource("test://resource", "Test Resource", "A test resource", - "text/plain", null); + McpSchema.Resource mockResource = McpSchema.Resource.builder() + .uri("test://resource") + .name("Test Resource") + .title("A Test Resource") + .description("A test resource") + .mimeType("text/plain") + .annotations(null) + .build(); McpSchema.ListResourcesResult mockResourcesResult = new McpSchema.ListResourcesResult(List.of(mockResource), null); @@ -229,6 +235,7 @@ void testResourcesChangeNotificationHandling() { assertThat(receivedResources).hasSize(1); assertThat(receivedResources.get(0).uri()).isEqualTo("test://resource"); assertThat(receivedResources.get(0).name()).isEqualTo("Test Resource"); + assertThat(receivedResources.get(0).title()).isEqualTo("A Test Resource"); assertThat(receivedResources.get(0).description()).isEqualTo("A test resource"); asyncMcpClient.closeGracefully(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index dd9f65895..2b331e699 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -212,8 +212,14 @@ void testAddResource() { .capabilities(ServerCapabilities.builder().resources(true, false).build()) .build(); - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); + Resource resource = Resource.builder() + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); @@ -244,8 +250,15 @@ void testAddResourceWithoutCapability() { .serverInfo("test-server", "1.0.0") .build(); - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); + Resource resource = Resource.builder() + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); + McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 6cbb8632c..25d8d3a81 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -208,8 +208,15 @@ void testAddResource() { .capabilities(ServerCapabilities.builder().resources(true, false).build()) .build(); - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); + Resource resource = Resource.builder() + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); + McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( resource, (exchange, req) -> new ReadResourceResult(List.of())); @@ -238,8 +245,15 @@ void testAddResourceWithoutCapability() { .serverInfo("test-server", "1.0.0") .build(); - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); + Resource resource = Resource.builder() + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); + McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( resource, (exchange, req) -> new ReadResourceResult(List.of())); diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 3968e7659..89908fa40 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -284,8 +284,14 @@ void testResource() throws Exception { McpSchema.Annotations annotations = new McpSchema.Annotations( Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.8); - McpSchema.Resource resource = new McpSchema.Resource("resource://test", "Test Resource", "A test resource", - "text/plain", annotations); + McpSchema.Resource resource = McpSchema.Resource.builder() + .uri("resource://test") + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("A test resource") + .annotations(annotations) + .build(); String value = mapper.writeValueAsString(resource); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -293,7 +299,7 @@ void testResource() throws Exception { .isObject() .isEqualTo( json(""" - {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}""")); + {"uri":"resource://test","name":"Test Resource","title":"A Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}""")); } @Test @@ -367,11 +373,21 @@ void testResourceTemplate() throws Exception { @Test void testListResourcesResult() throws Exception { - McpSchema.Resource resource1 = new McpSchema.Resource("resource://test1", "Test Resource 1", - "First test resource", "text/plain", null); + McpSchema.Resource resource1 = McpSchema.Resource.builder() + .uri("resource://test1") + .name("Test Resource 1") + .title("The first test resource") + .description("First test resource") + .mimeType("text/plain") + .build(); - McpSchema.Resource resource2 = new McpSchema.Resource("resource://test2", "Test Resource 2", - "Second test resource", "application/json", null); + McpSchema.Resource resource2 = McpSchema.Resource.builder() + .uri("resource://test2") + .name("Test Resource 2") + .title("The second test resource") + .description("Second test resource") + .mimeType("application/json") + .build(); McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2), "next-cursor"); @@ -382,7 +398,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","title":"The first test resource","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","title":"The second test resource","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); } @Test From 4d960a2c58b65c8e9bc271265f34e3b83542a920 Mon Sep 17 00:00:00 2001 From: YunKui Lu Date: Sat, 28 Jun 2025 19:26:13 +0800 Subject: [PATCH 2/5] feat: Added optional title field to `resource`, `tool`, `prompt` - Added unit tests Signed-off-by: YunKui Lu --- .../WebFluxSseIntegrationTests.java | 14 +- .../server/McpAsyncServer.java | 17 ++- .../modelcontextprotocol/spec/McpSchema.java | 45 +++++- .../McpAsyncClientResponseHandlerTests.java | 10 +- .../spec/McpSchemaTests.java | 144 ++++++++++++++---- 5 files changed, 193 insertions(+), 37 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 2f85654e8..0a2c07ed9 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -998,8 +998,12 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { var mcpServer = McpServer.sync(mcpServerTransportProvider) .capabilities(ServerCapabilities.builder().completions().build()) .prompts(new McpServerFeatures.SyncPromptSpecification( - new Prompt("code_review", "this is code review prompt", - List.of(new PromptArgument("language", "string", false))), + Prompt.builder() + .name("code_review") + .title("Request Code Review") + .description("this is code review prompt") + .arguments(List.of(new PromptArgument("language", "string", false))) + .build(), (mcpSyncServerExchange, getPromptRequest) -> null)) .completions(new McpServerFeatures.SyncCompletionSpecification( new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler)) @@ -1010,7 +1014,11 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); - CompleteRequest request = new CompleteRequest(new PromptReference("ref/prompt", "code_review"), + CompleteRequest request = new CompleteRequest(PromptReference.builder() + .type("ref/prompt") + .name("code_review") + .title("Request Code Review") + .build(), new CompleteRequest.CompleteArgument("language", "py")); CompleteResult result = mcpClient.completeCompletion(request); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 02ad955b9..fcd104644 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -462,9 +462,14 @@ private List getResourceTemplates() { .filter(uri -> uri.contains("{")) .map(uri -> { var resource = this.resources.get(uri).resource(); - var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(), resource.description(), - resource.mimeType(), resource.annotations()); - return template; + return ResourceTemplate.builder() + .uriTemplate(resource.uri()) + .name(resource.name()) + .title(resource.title()) + .description(resource.description()) + .mimeType(resource.mimeType()) + .annotations(resource.annotations()) + .build(); }) .toList(); @@ -725,7 +730,11 @@ private McpSchema.CompleteRequest parseCompletionParams(Object object) { String refType = (String) refMap.get("type"); McpSchema.CompleteReference ref = switch (refType) { - case "ref/prompt" -> new McpSchema.PromptReference(refType, (String) refMap.get("name")); + case "ref/prompt" -> McpSchema.PromptReference.builder() + .type(refType) + .name((String) refMap.get("name")) + .title((String) refMap.get("title")) + .build(); case "ref/resource" -> new McpSchema.ResourceReference(refType, (String) refMap.get("uri")); default -> throw new IllegalArgumentException("Invalid ref type: " + refType); }; diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 4fe7f2ad5..0880071d3 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1081,6 +1081,11 @@ public Builder inputSchema(JsonSchema inputSchema) { return this; } + public Builder inputSchema(String inputSchema) { + this.inputSchema = parseSchema(inputSchema); + return this; + } + public Builder annotations(ToolAnnotations annotations) { this.annotations = annotations; return this; @@ -1677,16 +1682,52 @@ public sealed interface CompleteReference permits PromptReference, ResourceRefer @JsonIgnoreProperties(ignoreUnknown = true) public record PromptReference(// @formatter:off @JsonProperty("type") String type, - @JsonProperty("name") String name) implements McpSchema.CompleteReference { + @JsonProperty("name") String name, + @JsonProperty("title") String title) implements McpSchema.CompleteReference { public PromptReference(String name) { - this("ref/prompt", name); + this("ref/prompt", name, null); + } + + public PromptReference(String type, String name) { + this(type, name, null); } @Override public String identifier() { return name(); } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String type = "ref/prompt"; + private String name; + private String title; + + private Builder(){ + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + public PromptReference build() { + return new PromptReference(type, name, title); + } + } }// @formatter:on @JsonInclude(JsonInclude.Include.NON_ABSENT) diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index 21d40aa7e..185191cf5 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -258,8 +258,13 @@ void testPromptsChangeNotificationHandling() { assertThat(asyncMcpClient.initialize().block()).isNotNull(); // Create a mock prompts list that the server will return - McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "Test Prompt Description", - List.of(new McpSchema.PromptArgument("arg1", "Test argument", true))); + McpSchema.Prompt mockPrompt = McpSchema.Prompt.builder() + .name("test-prompt") + .title("Test Prompt") + .description("Test Prompt Description") + .arguments(List.of(new McpSchema.PromptArgument("arg1", "Test argument", true))) + .build(); + McpSchema.ListPromptsResult mockPromptsResult = new McpSchema.ListPromptsResult(List.of(mockPrompt), null); // Simulate server sending prompts/list_changed notification @@ -278,6 +283,7 @@ void testPromptsChangeNotificationHandling() { // Verify the consumer received the expected prompts assertThat(receivedPrompts).hasSize(1); assertThat(receivedPrompts.get(0).name()).isEqualTo("test-prompt"); + assertThat(receivedPrompts.get(0).title()).isEqualTo("Test Prompt"); assertThat(receivedPrompts.get(0).description()).isEqualTo("Test Prompt Description"); assertThat(receivedPrompts.get(0).arguments()).hasSize(1); assertThat(receivedPrompts.get(0).arguments().get(0).name()).isEqualTo("arg1"); diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 89908fa40..b3b5a4c43 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -359,8 +359,13 @@ void testResourceBuilderNameRequired() { void testResourceTemplate() throws Exception { McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5); - McpSchema.ResourceTemplate template = new McpSchema.ResourceTemplate("resource://{param}/test", "Test Template", - "A test resource template", "text/plain", annotations); + McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() + .uriTemplate("resource://{param}/test") + .name("Test Template") + .description("A test resource template") + .mimeType("text/plain") + .annotations(annotations) + .build(); String value = mapper.writeValueAsString(template); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -371,12 +376,34 @@ void testResourceTemplate() throws Exception { {"uriTemplate":"resource://{param}/test","name":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}""")); } + @Test + void testResourceTemplateWithAllFields() throws Exception { + McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5); + + McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() + .uriTemplate("resource://{param}/test") + .name("Test Template") + .title("Test Resource Template") + .description("A test resource template") + .mimeType("text/plain") + .annotations(annotations) + .build(); + + String value = mapper.writeValueAsString(template); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"uriTemplate":"resource://{param}/test","name":"Test Template","title":"Test Resource Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}""")); + } + @Test void testListResourcesResult() throws Exception { McpSchema.Resource resource1 = McpSchema.Resource.builder() .uri("resource://test1") .name("Test Resource 1") - .title("The first test resource") +// .title("The first test resource") // No Title .description("First test resource") .mimeType("text/plain") .build(); @@ -398,16 +425,26 @@ void testListResourcesResult() throws Exception { .isObject() .isEqualTo( json(""" - {"resources":[{"uri":"resource://test1","name":"Test Resource 1","title":"The first test resource","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","title":"The second test resource","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","title":"The second test resource","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); } @Test void testListResourceTemplatesResult() throws Exception { - McpSchema.ResourceTemplate template1 = new McpSchema.ResourceTemplate("resource://{param}/test1", - "Test Template 1", "First test template", "text/plain", null); + McpSchema.ResourceTemplate template1 = McpSchema.ResourceTemplate.builder() + .uriTemplate("resource://{param}/test1") + .name("Test Template 1") + .title("First Test Template") + .description("First test template") + .mimeType("text/plain") + .build(); - McpSchema.ResourceTemplate template2 = new McpSchema.ResourceTemplate("resource://{param}/test2", - "Test Template 2", "Second test template", "application/json", null); + McpSchema.ResourceTemplate template2 = McpSchema.ResourceTemplate.builder() + .uriTemplate("resource://{param}/test2") + .name("Test Template 2") +// .title("Second Test Template") // No Title + .description("Second test template") + .mimeType("application/json") + .build(); McpSchema.ListResourceTemplatesResult result = new McpSchema.ListResourceTemplatesResult( Arrays.asList(template1, template2), "next-cursor"); @@ -418,7 +455,7 @@ void testListResourceTemplatesResult() throws Exception { .isObject() .isEqualTo( json(""" - {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); + {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","title":"First Test Template","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); } @Test @@ -456,11 +493,25 @@ void testReadResourceResult() throws Exception { @Test void testPrompt() throws Exception { - McpSchema.PromptArgument arg1 = new McpSchema.PromptArgument("arg1", "First argument", true); + McpSchema.PromptArgument arg1 = McpSchema.PromptArgument.builder() + .name("arg1") + .title("Arg1") + .description("First argument") + .required(true) + .build(); - McpSchema.PromptArgument arg2 = new McpSchema.PromptArgument("arg2", "Second argument", false); + McpSchema.PromptArgument arg2 = McpSchema.PromptArgument.builder() + .name("arg2") + .description("Second argument") + .required(false) + .build(); - McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "A test prompt", Arrays.asList(arg1, arg2)); + McpSchema.Prompt prompt = McpSchema.Prompt.builder() + .name("test-prompt") + .title("Test Prompt") + .description("A test prompt") + .arguments(Arrays.asList(arg1, arg2)) + .build(); String value = mapper.writeValueAsString(prompt); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -468,7 +519,7 @@ void testPrompt() throws Exception { .isObject() .isEqualTo( json(""" - {"name":"test-prompt","description":"A test prompt","arguments":[{"name":"arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}""")); + {"name":"test-prompt","title":"Test Prompt","description":"A test prompt","arguments":[{"name":"arg1","title":"Arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}""")); } @Test @@ -487,10 +538,21 @@ void testPromptMessage() throws Exception { @Test void testListPromptsResult() throws Exception { - McpSchema.PromptArgument arg = new McpSchema.PromptArgument("arg", "An argument", true); + McpSchema.PromptArgument arg = McpSchema.PromptArgument.builder() + .name("arg") + .title("Arg") + .description("An argument") + .required(true) + .build(); - McpSchema.Prompt prompt1 = new McpSchema.Prompt("prompt1", "First prompt", Collections.singletonList(arg)); + McpSchema.Prompt prompt1 = McpSchema.Prompt.builder() + .name("prompt1") + .title("A first prompt") + .description("First prompt") + .arguments(Collections.singletonList(arg)) + .build(); + // - No Title Prompt McpSchema.Prompt prompt2 = new McpSchema.Prompt("prompt2", "Second prompt", Collections.emptyList()); McpSchema.ListPromptsResult result = new McpSchema.ListPromptsResult(Arrays.asList(prompt1, prompt2), @@ -502,7 +564,7 @@ void testListPromptsResult() throws Exception { .isObject() .isEqualTo( json(""" - {"prompts":[{"name":"prompt1","description":"First prompt","arguments":[{"name":"arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}""")); + {"prompts":[{"name":"prompt1","title":"A first prompt","description":"First prompt","arguments":[{"name":"arg","title":"Arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}""")); } @Test @@ -645,7 +707,12 @@ void testTool() throws Exception { } """; - McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson); + McpSchema.Tool tool = McpSchema.Tool.builder() + .name("test-tool") + .title("Test Tool") + .description("A test tool") + .inputSchema(schemaJson) + .build(); String value = mapper.writeValueAsString(tool); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -653,7 +720,7 @@ void testTool() throws Exception { .isObject() .isEqualTo( json(""" - {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}""")); + {"name":"test-tool","title":"Test Tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}""")); } @Test @@ -679,7 +746,12 @@ void testToolWithComplexSchema() throws Exception { } """; - McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson); + McpSchema.Tool tool = McpSchema.Tool.builder() + .name("addressTool") + .title("Address Tool") + .description("Handles addresses") + .inputSchema(complexSchemaJson) + .build(); // Serialize the tool to a string String serialized = mapper.writeValueAsString(tool); @@ -717,7 +789,13 @@ void testToolWithAnnotations() throws Exception { McpSchema.ToolAnnotations annotations = new McpSchema.ToolAnnotations("A test tool", false, false, false, false, false); - McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson, annotations); + McpSchema.Tool tool = McpSchema.Tool.builder() + .name("test-tool") + .title("Test Tool") + .description("A test tool") + .inputSchema(schemaJson) + .annotations(annotations) + .build(); String value = mapper.writeValueAsString(tool); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -725,7 +803,7 @@ void testToolWithAnnotations() throws Exception { .isObject() .isEqualTo( json(""" - {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]},"annotations":{"title":"A test tool","readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false,"returnDirect":false}}""")); + {"name":"test-tool","title":"Test Tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]},"annotations":{"title":"A test tool","readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false,"returnDirect":false}}""")); } @Test @@ -984,21 +1062,35 @@ void testCreateElicitationResult() throws Exception { @Test void testRoot() throws Exception { - McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root"); + McpSchema.Root root = McpSchema.Root.builder() + .uri("file:///path/to/root") + .name("Test Root") + .title("A test root") + .build(); 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","title":"A test root"}""")); } @Test void testListRootsResult() throws Exception { - McpSchema.Root root1 = new McpSchema.Root("file:///path/to/root1", "First Root"); + McpSchema.Root root1 = McpSchema.Root.builder() + .uri("file:///path/to/root1") + .name("First Root") + .title("First Root Title") + .build(); + + + McpSchema.Root root2 = McpSchema.Root.builder() + .uri("file:///path/to/root2") + .name("Second Root") +// .title("Second Root Title") // no title + .build(); - McpSchema.Root root2 = new McpSchema.Root("file:///path/to/root2", "Second Root"); McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2), "next-cursor"); @@ -1009,7 +1101,7 @@ void testListRootsResult() throws Exception { .isObject() .isEqualTo( json(""" - {"roots":[{"uri":"file:///path/to/root1","name":"First Root"},{"uri":"file:///path/to/root2","name":"Second Root"}],"nextCursor":"next-cursor"}""")); + {"roots":[{"uri":"file:///path/to/root1","name":"First Root","title":"First Root Title"},{"uri":"file:///path/to/root2","name":"Second Root"}],"nextCursor":"next-cursor"}""")); } From a124b1e07e3067c12f94e63143f0a1dd2d429f34 Mon Sep 17 00:00:00 2001 From: YunKui Lu Date: Sat, 28 Jun 2025 19:29:45 +0800 Subject: [PATCH 3/5] feat: Added optional title field to `resource`, `tool`, `prompt` - add backwards-compatibility for Tool Signed-off-by: YunKui Lu --- .../java/io/modelcontextprotocol/spec/McpSchema.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 0880071d3..24046c304 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1047,6 +1047,15 @@ public Tool(String name, String description, String schema, ToolAnnotations anno this(name, null, description, parseSchema(schema), annotations); } + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Tool#builder()} instead. + */ + @Deprecated + public Tool(String name, String description, JsonSchema inputSchema, ToolAnnotations annotations) { + this(name, null, description, inputSchema, annotations); + } + public static Builder builder() { return new Builder(); } From 287d2cf10e03538aae806637579ab7b7bb9e491f Mon Sep 17 00:00:00 2001 From: YunKui Lu Date: Sat, 28 Jun 2025 19:33:35 +0800 Subject: [PATCH 4/5] run `mvn spring-javaformat:apply` Signed-off-by: YunKui Lu --- .../WebFluxSseIntegrationTests.java | 23 ++--- .../server/McpAsyncServer.java | 24 ++--- .../McpAsyncClientResponseHandlerTests.java | 24 ++--- .../server/AbstractMcpAsyncServerTests.java | 44 ++++++--- .../server/AbstractMcpSyncServerTests.java | 28 +++--- .../spec/McpSchemaTests.java | 98 +++++++++---------- 6 files changed, 124 insertions(+), 117 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 0a2c07ed9..1fc8607e3 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -997,14 +997,12 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { var mcpServer = McpServer.sync(mcpServerTransportProvider) .capabilities(ServerCapabilities.builder().completions().build()) - .prompts(new McpServerFeatures.SyncPromptSpecification( - Prompt.builder() - .name("code_review") - .title("Request Code Review") - .description("this is code review prompt") - .arguments(List.of(new PromptArgument("language", "string", false))) - .build(), - (mcpSyncServerExchange, getPromptRequest) -> null)) + .prompts(new McpServerFeatures.SyncPromptSpecification(Prompt.builder() + .name("code_review") + .title("Request Code Review") + .description("this is code review prompt") + .arguments(List.of(new PromptArgument("language", "string", false))) + .build(), (mcpSyncServerExchange, getPromptRequest) -> null)) .completions(new McpServerFeatures.SyncCompletionSpecification( new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler)) .build(); @@ -1015,11 +1013,10 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { assertThat(initResult).isNotNull(); CompleteRequest request = new CompleteRequest(PromptReference.builder() - .type("ref/prompt") - .name("code_review") - .title("Request Code Review") - .build(), - new CompleteRequest.CompleteArgument("language", "py")); + .type("ref/prompt") + .name("code_review") + .title("Request Code Review") + .build(), new CompleteRequest.CompleteArgument("language", "py")); CompleteResult result = mcpClient.completeCompletion(request); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index fcd104644..73c554fb9 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -462,14 +462,14 @@ private List getResourceTemplates() { .filter(uri -> uri.contains("{")) .map(uri -> { var resource = this.resources.get(uri).resource(); - return ResourceTemplate.builder() - .uriTemplate(resource.uri()) - .name(resource.name()) - .title(resource.title()) - .description(resource.description()) - .mimeType(resource.mimeType()) - .annotations(resource.annotations()) - .build(); + return ResourceTemplate.builder() + .uriTemplate(resource.uri()) + .name(resource.name()) + .title(resource.title()) + .description(resource.description()) + .mimeType(resource.mimeType()) + .annotations(resource.annotations()) + .build(); }) .toList(); @@ -731,10 +731,10 @@ private McpSchema.CompleteRequest parseCompletionParams(Object object) { McpSchema.CompleteReference ref = switch (refType) { case "ref/prompt" -> McpSchema.PromptReference.builder() - .type(refType) - .name((String) refMap.get("name")) - .title((String) refMap.get("title")) - .build(); + .type(refType) + .name((String) refMap.get("name")) + .title((String) refMap.get("title")) + .build(); case "ref/resource" -> new McpSchema.ResourceReference(refType, (String) refMap.get("uri")); default -> throw new IllegalArgumentException("Invalid ref type: " + refType); }; diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index 185191cf5..f231d80eb 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -208,13 +208,13 @@ void testResourcesChangeNotificationHandling() { // Create a mock resources list that the server will return McpSchema.Resource mockResource = McpSchema.Resource.builder() - .uri("test://resource") - .name("Test Resource") - .title("A Test Resource") - .description("A test resource") - .mimeType("text/plain") - .annotations(null) - .build(); + .uri("test://resource") + .name("Test Resource") + .title("A Test Resource") + .description("A test resource") + .mimeType("text/plain") + .annotations(null) + .build(); McpSchema.ListResourcesResult mockResourcesResult = new McpSchema.ListResourcesResult(List.of(mockResource), null); @@ -259,11 +259,11 @@ void testPromptsChangeNotificationHandling() { // Create a mock prompts list that the server will return McpSchema.Prompt mockPrompt = McpSchema.Prompt.builder() - .name("test-prompt") - .title("Test Prompt") - .description("Test Prompt Description") - .arguments(List.of(new McpSchema.PromptArgument("arg1", "Test argument", true))) - .build(); + .name("test-prompt") + .title("Test Prompt") + .description("Test Prompt Description") + .arguments(List.of(new McpSchema.PromptArgument("arg1", "Test argument", true))) + .build(); McpSchema.ListPromptsResult mockPromptsResult = new McpSchema.ListPromptsResult(List.of(mockPrompt), null); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index 2b331e699..68ba0dda1 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -213,13 +213,13 @@ void testAddResource() { .build(); Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("A Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .annotations(null) - .build(); + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); @@ -251,13 +251,13 @@ void testAddResourceWithoutCapability() { .build(); Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("A Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .annotations(null) - .build(); + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); @@ -314,7 +314,13 @@ void testAddPromptWithoutCapability() { .serverInfo("test-server", "1.0.0") .build(); - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); + Prompt prompt = Prompt.builder() + .name(TEST_PROMPT_NAME) + .title("A Test Prompt") + .description("Test Prompt") + .arguments(List.of()) + .build(); + McpServerFeatures.AsyncPromptSpecification specification = new McpServerFeatures.AsyncPromptSpecification( prompt, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); @@ -342,7 +348,13 @@ void testRemovePromptWithoutCapability() { void testRemovePrompt() { String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678"; - Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of()); + Prompt prompt = Prompt.builder() + .name(TEST_PROMPT_NAME_TO_REMOVE) + .title("Test Prompt Name 678") + .description("Test Prompt") + .arguments(List.of()) + .build(); + McpServerFeatures.AsyncPromptSpecification specification = new McpServerFeatures.AsyncPromptSpecification( prompt, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 25d8d3a81..92fa1657c 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -209,13 +209,13 @@ void testAddResource() { .build(); Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("A Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .annotations(null) - .build(); + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( resource, (exchange, req) -> new ReadResourceResult(List.of())); @@ -246,13 +246,13 @@ void testAddResourceWithoutCapability() { .build(); Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("A Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .annotations(null) - .build(); + .uri(TEST_RESOURCE_URI) + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("Test resource description") + .annotations(null) + .build(); McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( resource, (exchange, req) -> new ReadResourceResult(List.of())); diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index b3b5a4c43..36dc290d0 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -285,13 +285,13 @@ void testResource() throws Exception { Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.8); McpSchema.Resource resource = McpSchema.Resource.builder() - .uri("resource://test") - .name("Test Resource") - .title("A Test Resource") - .mimeType("text/plain") - .description("A test resource") - .annotations(annotations) - .build(); + .uri("resource://test") + .name("Test Resource") + .title("A Test Resource") + .mimeType("text/plain") + .description("A test resource") + .annotations(annotations) + .build(); String value = mapper.writeValueAsString(resource); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -403,7 +403,7 @@ void testListResourcesResult() throws Exception { McpSchema.Resource resource1 = McpSchema.Resource.builder() .uri("resource://test1") .name("Test Resource 1") -// .title("The first test resource") // No Title + // .title("The first test resource") // No Title .description("First test resource") .mimeType("text/plain") .build(); @@ -441,7 +441,7 @@ void testListResourceTemplatesResult() throws Exception { McpSchema.ResourceTemplate template2 = McpSchema.ResourceTemplate.builder() .uriTemplate("resource://{param}/test2") .name("Test Template 2") -// .title("Second Test Template") // No Title + // .title("Second Test Template") // No Title .description("Second test template") .mimeType("application/json") .build(); @@ -539,18 +539,18 @@ void testPromptMessage() throws Exception { @Test void testListPromptsResult() throws Exception { McpSchema.PromptArgument arg = McpSchema.PromptArgument.builder() - .name("arg") - .title("Arg") - .description("An argument") - .required(true) - .build(); + .name("arg") + .title("Arg") + .description("An argument") + .required(true) + .build(); McpSchema.Prompt prompt1 = McpSchema.Prompt.builder() - .name("prompt1") - .title("A first prompt") - .description("First prompt") - .arguments(Collections.singletonList(arg)) - .build(); + .name("prompt1") + .title("A first prompt") + .description("First prompt") + .arguments(Collections.singletonList(arg)) + .build(); // - No Title Prompt McpSchema.Prompt prompt2 = new McpSchema.Prompt("prompt2", "Second prompt", Collections.emptyList()); @@ -708,11 +708,11 @@ void testTool() throws Exception { """; McpSchema.Tool tool = McpSchema.Tool.builder() - .name("test-tool") - .title("Test Tool") - .description("A test tool") - .inputSchema(schemaJson) - .build(); + .name("test-tool") + .title("Test Tool") + .description("A test tool") + .inputSchema(schemaJson) + .build(); String value = mapper.writeValueAsString(tool); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -747,11 +747,11 @@ void testToolWithComplexSchema() throws Exception { """; McpSchema.Tool tool = McpSchema.Tool.builder() - .name("addressTool") - .title("Address Tool") - .description("Handles addresses") - .inputSchema(complexSchemaJson) - .build(); + .name("addressTool") + .title("Address Tool") + .description("Handles addresses") + .inputSchema(complexSchemaJson) + .build(); // Serialize the tool to a string String serialized = mapper.writeValueAsString(tool); @@ -790,12 +790,12 @@ void testToolWithAnnotations() throws Exception { false); McpSchema.Tool tool = McpSchema.Tool.builder() - .name("test-tool") - .title("Test Tool") - .description("A test tool") - .inputSchema(schemaJson) - .annotations(annotations) - .build(); + .name("test-tool") + .title("Test Tool") + .description("A test tool") + .inputSchema(schemaJson) + .annotations(annotations) + .build(); String value = mapper.writeValueAsString(tool); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -1062,11 +1062,11 @@ void testCreateElicitationResult() throws Exception { @Test void testRoot() throws Exception { - McpSchema.Root root = McpSchema.Root.builder() - .uri("file:///path/to/root") - .name("Test Root") - .title("A test root") - .build(); + McpSchema.Root root = McpSchema.Root.builder() + .uri("file:///path/to/root") + .name("Test Root") + .title("A test root") + .build(); String value = mapper.writeValueAsString(root); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -1079,18 +1079,16 @@ void testRoot() throws Exception { @Test void testListRootsResult() throws Exception { McpSchema.Root root1 = McpSchema.Root.builder() - .uri("file:///path/to/root1") - .name("First Root") - .title("First Root Title") - .build(); - + .uri("file:///path/to/root1") + .name("First Root") + .title("First Root Title") + .build(); McpSchema.Root root2 = McpSchema.Root.builder() - .uri("file:///path/to/root2") - .name("Second Root") -// .title("Second Root Title") // no title - .build(); - + .uri("file:///path/to/root2") + .name("Second Root") + // .title("Second Root Title") // no title + .build(); McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2), "next-cursor"); From 908af0fb9c9425d357b9bcc999b3100221f597b8 Mon Sep 17 00:00:00 2001 From: YunKui Lu Date: Tue, 1 Jul 2025 22:40:45 +0800 Subject: [PATCH 5/5] feat: Added optional title field to `resource`, `tool`, `prompt` - add backwards-compatibility for Tool - Added unit tests Signed-off-by: YunKui Lu --- mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 6e5a87957..31ab0837a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1070,8 +1070,8 @@ public record ToolAnnotations( // @formatter:off @JsonIgnoreProperties(ignoreUnknown = true) public record Tool( // @formatter:off @JsonProperty("name") String name, + @JsonProperty("title") String title, @JsonProperty("description") String description, - @JsonProperty("title") String title, @JsonProperty("inputSchema") JsonSchema inputSchema, @JsonProperty("annotations") ToolAnnotations annotations) {