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 73aff462..a02a015b 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 @@ -995,10 +995,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))), - (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(); @@ -1008,8 +1010,11 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); - CompleteRequest request = new CompleteRequest(new PromptReference("ref/prompt", "code_review"), - new CompleteRequest.CompleteArgument("language", "py")); + 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 02ad955b..73c554fb 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 9be585ce..31ab0837 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -248,7 +248,7 @@ public record InitializeRequest( // @formatter:off @JsonProperty("capabilities") ClientCapabilities capabilities, @JsonProperty("clientInfo") Implementation clientInfo, @JsonProperty("_meta") Map meta) implements Request { - + public InitializeRequest(String protocolVersion, ClientCapabilities capabilities, Implementation clientInfo) { this(protocolVersion, capabilities, clientInfo, null); } @@ -338,6 +338,9 @@ public static class Builder { private Sampling sampling; private Elicitation elicitation; + private Builder() { + } + public Builder experimental(Map experimental) { this.experimental = experimental; return this; @@ -411,6 +414,9 @@ public static class Builder { private ResourceCapabilities resources; private ToolCapabilities tools; + private Builder() { + } + public Builder completions() { this.completions = new CompletionCapabilities(); return this; @@ -519,8 +525,12 @@ public interface ResourceContent { * 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. @@ -536,6 +546,7 @@ public interface ResourceContent { 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, @@ -547,7 +558,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() { @@ -557,11 +568,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; @@ -572,6 +587,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; @@ -596,7 +616,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 @@ -609,6 +629,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. @@ -622,9 +646,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) @@ -739,6 +823,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. */ @@ -746,8 +834,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 /** @@ -761,8 +896,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 /** @@ -872,6 +1055,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 @@ -883,18 +1070,85 @@ 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. + */ public Tool(String name, String description, String schema, ToolAnnotations annotations) { - this(name, description, parseSchema(schema), annotations); + 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(); + } + + 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 inputSchema(String inputSchema) { + this.inputSchema = parseSchema(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) { @@ -1023,6 +1277,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 @@ -1114,6 +1371,9 @@ public static class Builder { private Double speedPriority; private Double intelligencePriority; + private Builder() { + } + public Builder hints(List hints) { this.hints = hints; return this; @@ -1177,7 +1437,7 @@ public record CreateMessageRequest(// @formatter:off @JsonProperty("metadata") Map metadata, @JsonProperty("_meta") Map meta) implements Request { - + // backwards compatibility constructor public CreateMessageRequest(List messages, ModelPreferences modelPreferences, String systemPrompt, ContextInclusionStrategy includeContext, @@ -1208,6 +1468,9 @@ public static class Builder { private Map metadata; private Map meta; + private Builder() { + } + public Builder messages(List messages) { this.messages = messages; return this; @@ -1307,6 +1570,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; @@ -1366,6 +1632,9 @@ public static class Builder { private Map requestedSchema; private Map meta; + private Builder() { + } + public Builder message(String message) { this.message = message; return this; @@ -1415,6 +1684,9 @@ public static class Builder { private Action action; private Map content; + private Builder() { + } + public Builder message(Action action) { this.action = action; return this; @@ -1516,6 +1788,9 @@ public static class Builder { private String logger = "server"; private String data; + private Builder() { + } + public Builder level(LoggingLevel level) { this.level = level; return this; @@ -1579,16 +1854,57 @@ 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); + } + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Tool#builder()} instead. + */ + @Deprecated + 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) @@ -1605,6 +1921,37 @@ public ResourceReference(String uri) { public String identifier() { return uri(); } + + 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) @@ -1870,12 +2217,57 @@ 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 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 3e89c8ce..fe529556 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(); @@ -251,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 @@ -271,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/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index dd9f6589..68ba0dda 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()))); @@ -301,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")))))); @@ -329,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 6cbb8632..92fa1657 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 ea063e4e..cee51a77 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -349,8 +349,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) @@ -358,7 +364,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 @@ -418,8 +424,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) @@ -430,13 +441,45 @@ 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 = 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") // No Title + .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"); @@ -447,16 +490,26 @@ 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","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"); @@ -467,7 +520,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 @@ -535,11 +588,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) @@ -547,7 +614,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 @@ -566,10 +633,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), @@ -581,7 +659,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 @@ -748,7 +826,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) @@ -756,7 +839,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 @@ -782,7 +865,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); @@ -820,7 +908,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) @@ -828,7 +922,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 @@ -1281,21 +1375,33 @@ void testCompleteRequestWithMeta() 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 = new McpSchema.Root("file:///path/to/root2", "Second Root"); + McpSchema.Root root2 = McpSchema.Root.builder() + .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"); @@ -1306,7 +1412,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"}""")); }