Skip to content

feat: Add titles field #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -996,19 +996,20 @@ 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))),
new Prompt("code_review", "Code review", "this is code review prompt",
List.of(new PromptArgument("language", "Language", "string", false))),
(mcpSyncServerExchange, getPromptRequest) -> null))
.completions(new McpServerFeatures.SyncCompletionSpecification(
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
new McpSchema.PromptReference("ref/prompt", "code_review", "Code review"), completionHandler))
.build();

try (var mcpClient = clientBuilder.build()) {

InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();

CompleteRequest request = new CompleteRequest(new PromptReference("ref/prompt", "code_review"),
CompleteRequest request = new CompleteRequest(
new PromptReference("ref/prompt", "code_review", "Code review"),
new CompleteRequest.CompleteArgument("language", "py"));

CompleteResult result = mcpClient.completeCompletion(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ void testListAllPromptsReturnsImmutableList() {
.consumeNextWith(result -> {
assertThat(result.prompts()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", null)))
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "Test", "test", null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
Expand Down Expand Up @@ -604,7 +604,7 @@ void testListAllResourceTemplatesReturnsImmutableList() {
assertThat(result.resourceTemplates()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.resourceTemplates()
.add(new McpSchema.ResourceTemplate("test://template", "test", null, null, null)))
.add(new McpSchema.ResourceTemplate("test://template", "test", "test", null, null, null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ void testAddPromptWithoutCapability() {
.serverInfo("test-server", "1.0.0")
.build();

Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of());
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"))))));
Expand Down Expand Up @@ -330,7 +330,7 @@ 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 = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", "Test Prompt", List.of());
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"))))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ void testAddPromptWithoutCapability() {
.serverInfo("test-server", "1.0.0")
.build();

Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of());
McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
Expand All @@ -310,7 +310,7 @@ void testRemovePromptWithoutCapability() {

@Test
void testRemovePrompt() {
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of());
McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,8 @@ private List<McpSchema.ResourceTemplate> 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());
var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(), resource.title(),
resource.description(), resource.mimeType(), resource.annotations());
return template;
})
.toList();
Expand Down Expand Up @@ -725,7 +725,8 @@ 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" -> new McpSchema.PromptReference(refType, (String) refMap.get("name"),
refMap.get("title") != null ? (String) refMap.get("title") : null);
case "ref/resource" -> new McpSchema.ResourceReference(refType, (String) refMap.get("uri"));
default -> throw new IllegalArgumentException("Invalid ref type: " + refType);
};
Expand Down
119 changes: 104 additions & 15 deletions mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
*
* @author Christian Tzolov
* @author Luca Chang
* @author Surbhi Bansal
*/
public final class McpSchema {

Expand Down Expand Up @@ -451,7 +452,12 @@ public ServerCapabilities build() {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Implementation(// @formatter:off
@JsonProperty("name") String name,
@JsonProperty("version") String version) {
@JsonProperty("title") String title,
@JsonProperty("version") String version) implements BaseMetadata {

public Implementation(String name, String version) {
this(name, null, version);
}
} // @formatter:on

// Existing Enums and Base Types (from previous implementation)
Expand Down Expand Up @@ -499,12 +505,10 @@ public record Annotations( // @formatter:off
* interface is implemented by both {@link Resource} and {@link ResourceLink} to
* provide a consistent way to access resource metadata.
*/
public interface ResourceContent {
public interface ResourceContent extends BaseMetadata {

String uri();

String name();

String description();

String mimeType();
Expand All @@ -515,6 +519,28 @@ public interface ResourceContent {

}

/**
* Base interface for metadata with name (identifier) and title (display name)
* properties.
*/
public interface BaseMetadata {

/**
* Intended for programmatic or logical use, but used as a display name in past
* specs or fallback (if title isn't present).
*/
String name();

/**
* 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.
*/
String title();

}

/**
* A known resource that the server is capable of reading.
*
Expand All @@ -536,6 +562,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,
Expand All @@ -547,7 +574,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() {
Expand All @@ -557,6 +584,7 @@ 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;
Expand All @@ -572,6 +600,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;
Expand All @@ -596,7 +629,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
Expand All @@ -622,9 +655,10 @@ 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 {
@JsonProperty("annotations") Annotations annotations) implements Annotated, BaseMetadata {
} // @formatter:on

@JsonInclude(JsonInclude.Include.NON_ABSENT)
Expand Down Expand Up @@ -746,8 +780,9 @@ 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<PromptArgument> arguments) {
@JsonProperty("arguments") List<PromptArgument> arguments) implements BaseMetadata {
} // @formatter:on

/**
Expand All @@ -761,8 +796,9 @@ 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) {
@JsonProperty("required") Boolean required) implements BaseMetadata {
}// @formatter:on

/**
Expand Down Expand Up @@ -883,16 +919,60 @@ 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) {
@JsonProperty("annotations") ToolAnnotations annotations) implements BaseMetadata {

public Tool(String name, String description, String schema) {
this(name, description, parseSchema(schema), null);
this(name, null, description, parseSchema(schema), null);
}

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;

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() {
Assert.hasText(name, "name must not be empty");

return new Tool(name, title, description, inputSchema, annotations);
}
}

} // @formatter:on
Expand Down Expand Up @@ -1579,10 +1659,11 @@ 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, BaseMetadata {

public PromptReference(String name) {
this("ref/prompt", name);
this("ref/prompt", name, null);
}

@Override
Expand Down Expand Up @@ -1794,6 +1875,7 @@ public Double priority() {
@JsonIgnoreProperties(ignoreUnknown = true)
public record ResourceLink( // @formatter:off
@JsonProperty("name") String name,
@JsonProperty("title") String title,
@JsonProperty("uri") String uri,
@JsonProperty("description") String description,
@JsonProperty("mimeType") String mimeType,
Expand All @@ -1808,6 +1890,8 @@ public static class Builder {

private String name;

private String title;

private String uri;

private String description;
Expand All @@ -1823,6 +1907,11 @@ public Builder name(String name) {
return this;
}

public Builder title(String title) {
this.title = title;
return this;
}

public Builder uri(String uri) {
this.uri = uri;
return this;
Expand Down Expand Up @@ -1852,7 +1941,7 @@ public ResourceLink build() {
Assert.hasText(uri, "uri must not be empty");
Assert.hasText(name, "name must not be empty");

return new ResourceLink(name, uri, description, mimeType, size, annotations);
return new ResourceLink(name, title, uri, description, mimeType, size, annotations);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ void testListAllPromptsReturnsImmutableList() {
.consumeNextWith(result -> {
assertThat(result.prompts()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", null)))
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", "test", null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
Expand Down Expand Up @@ -605,7 +605,7 @@ void testListAllResourceTemplatesReturnsImmutableList() {
assertThat(result.resourceTemplates()).isNotNull();
// Verify that the returned list is immutable
assertThatThrownBy(() -> result.resourceTemplates()
.add(new McpSchema.ResourceTemplate("test://template", "test", null, null, null)))
.add(new McpSchema.ResourceTemplate("test://template", "test", "test", null, null, null)))
.isInstanceOf(UnsupportedOperationException.class);
})
.verifyComplete();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ 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 = new McpSchema.Prompt("test-prompt", "Test Prompt", "Test Prompt Description",
List.of(new McpSchema.PromptArgument("arg1", "Test argument", "Test argument", true)));
McpSchema.ListPromptsResult mockPromptsResult = new McpSchema.ListPromptsResult(List.of(mockPrompt), null);

// Simulate server sending prompts/list_changed notification
Expand Down
Loading
Loading