Skip to content
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
43 changes: 39 additions & 4 deletions mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -577,14 +577,23 @@ public InitializeResult build() {
* @param roots Present if the client supports listing roots
* @param sampling Present if the client supports sampling from an LLM
* @param elicitation Present if the client supports elicitation from the server
* @param extensions Present if the client supports MCP extensions, keyed by extension
* identifier
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ClientCapabilities( // @formatter:off
@JsonProperty("experimental") Map<String, Object> experimental,
@JsonProperty("roots") RootCapabilities roots,
@JsonProperty("sampling") Sampling sampling,
@JsonProperty("elicitation") Elicitation elicitation) { // @formatter:on
@JsonProperty("elicitation") Elicitation elicitation,
@JsonProperty("extensions") Map<String, Object> extensions) { // @formatter:on

// Keep the old constructor so existing callers still compile
public ClientCapabilities(Map<String, Object> experimental, RootCapabilities roots, Sampling sampling,
Elicitation elicitation) {
this(experimental, roots, sampling, elicitation, null);
}

/**
* Present if the client supports listing roots.
Expand Down Expand Up @@ -723,6 +732,8 @@ public static class Builder {

private Elicitation elicitation;

private Map<String, Object> extensions;

public Builder experimental(Map<String, Object> experimental) {
this.experimental = experimental;
return this;
Expand Down Expand Up @@ -765,8 +776,13 @@ public Builder elicitation(Elicitation elicitation) {
return this;
}

public Builder extensions(Map<String, Object> extensions) {
this.extensions = extensions;
return this;
}

public ClientCapabilities build() {
return new ClientCapabilities(experimental, roots, sampling, elicitation);
return new ClientCapabilities(experimental, roots, sampling, elicitation, extensions);
}

}
Expand All @@ -785,6 +801,8 @@ public ClientCapabilities build() {
* @param prompts Present if the server offers any prompt templates
* @param resources Present if the server offers any resources to read
* @param tools Present if the server offers any tools to call
* @param extensions Present if the server supports MCP extensions, keyed by extension
* identifier
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -794,7 +812,15 @@ public record ServerCapabilities( // @formatter:off
@JsonProperty("logging") LoggingCapabilities logging,
@JsonProperty("prompts") PromptCapabilities prompts,
@JsonProperty("resources") ResourceCapabilities resources,
@JsonProperty("tools") ToolCapabilities tools) { // @formatter:on
@JsonProperty("tools") ToolCapabilities tools,
@JsonProperty("extensions") Map<String, Object> extensions) { // @formatter:on

// Keep the old constructor so existing callers still compile
public ServerCapabilities(CompletionCapabilities completions, Map<String, Object> experimental,
LoggingCapabilities logging, PromptCapabilities prompts, ResourceCapabilities resources,
ToolCapabilities tools) {
this(completions, experimental, logging, prompts, resources, tools, null);
}

/**
* Present if the server supports argument autocompletion suggestions.
Expand Down Expand Up @@ -923,6 +949,7 @@ public Builder mutate() {
builder.prompts = this.prompts;
builder.resources = this.resources;
builder.tools = this.tools;
builder.extensions = this.extensions;
return builder;
}

Expand All @@ -944,6 +971,8 @@ public static class Builder {

private ToolCapabilities tools;

private Map<String, Object> extensions;

public Builder completions() {
this.completions = new CompletionCapabilities();
return this;
Expand Down Expand Up @@ -974,8 +1003,14 @@ public Builder tools(Boolean listChanged) {
return this;
}

public Builder extensions(Map<String, Object> extensions) {
this.extensions = extensions;
return this;
}

public ServerCapabilities build() {
return new ServerCapabilities(completions, experimental, logging, prompts, resources, tools);
return new ServerCapabilities(completions, experimental, logging, prompts, resources, tools,
extensions);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.io.IOException;
import java.util.List;
import java.util.Map;

import io.modelcontextprotocol.json.McpJsonMapper;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -143,6 +144,75 @@ void serverCapabilitiesUnknownFieldsIgnored() throws IOException {
assertThat(caps.tools().listChanged()).isTrue();
}

@Test
void serverCapabilitiesWithoutExtensionsDeserializesAsNull() throws IOException {
String json = """
{"tools":{"listChanged":true}}
""";
McpSchema.ServerCapabilities caps = mapper.readValue(json, McpSchema.ServerCapabilities.class);
assertThat(caps.extensions()).isNull();
}

@Test
void serverCapabilitiesWithNullExtensionsOmitsFieldOnWire() throws IOException {
String json = mapper.writeValueAsString(McpSchema.ServerCapabilities.builder().tools(true).build());
assertThat(json).doesNotContain("extensions");
}

@Test
void serverCapabilitiesExtensionsRoundTrip() throws IOException {
Map<String, Object> extensions = Map.of("com.example/ext-with-settings", Map.of("maxDepth", 3),
"com.example/ext-without-settings", Map.of());
McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().extensions(extensions).build();

String json = mapper.writeValueAsString(caps);
assertThat(json).contains("\"com.example/ext-without-settings\":{}");

McpSchema.ServerCapabilities parsed = mapper.readValue(json, McpSchema.ServerCapabilities.class);
assertThat(parsed.extensions()).isEqualTo(extensions);
}

// -----------------------------------------------------------------------
// ClientCapabilities.extensions
// -----------------------------------------------------------------------

@Test
void clientCapabilitiesWithoutExtensionsDeserializesAsNull() throws IOException {
String json = """
{"roots":{"listChanged":true},"sampling":{}}
""";
McpSchema.ClientCapabilities caps = mapper.readValue(json, McpSchema.ClientCapabilities.class);
assertThat(caps.extensions()).isNull();
}

@Test
void clientCapabilitiesWithNullExtensionsOmitsFieldOnWire() throws IOException {
String json = mapper.writeValueAsString(McpSchema.ClientCapabilities.builder().sampling().build());
assertThat(json).doesNotContain("extensions");
}

@Test
void clientCapabilitiesWithExtensionsUnknownFieldsIgnored() throws IOException {
String json = """
{"extensions":{"io.modelcontextprotocol/tasks":{}},"futureCapability":{"a":1}}
""";
McpSchema.ClientCapabilities caps = mapper.readValue(json, McpSchema.ClientCapabilities.class);
assertThat(caps.extensions()).containsOnlyKeys("io.modelcontextprotocol/tasks");
}

@Test
void clientCapabilitiesExtensionsRoundTrip() throws IOException {
Map<String, Object> extensions = Map.of("com.example/ext-with-settings", Map.of("maxDepth", 3),
"com.example/ext-without-settings", Map.of());
McpSchema.ClientCapabilities caps = McpSchema.ClientCapabilities.builder().extensions(extensions).build();

String json = mapper.writeValueAsString(caps);
assertThat(json).contains("\"com.example/ext-without-settings\":{}");

McpSchema.ClientCapabilities parsed = mapper.readValue(json, McpSchema.ClientCapabilities.class);
assertThat(parsed.extensions()).isEqualTo(extensions);
}

// -----------------------------------------------------------------------
// JSONRPCError
// -----------------------------------------------------------------------
Expand Down