feat!: enforce required MCP spec fields in McpSchema; lenient wire deserialization (#928)

Every wire-serialized record in McpSchema now validates spec-required
fields at construction time. Wire deserialization is intentionally
lenient: a missing required field is replaced with a documented default
and a WARN is logged, instead of failing the parse. A required-first
builder convention is introduced across the schema so it is no longer
possible to obtain a builder that is missing required state.

Required-field guards
---------------------
Every wire record's compact constructor asserts non-null (and non-empty
for String identifiers like name, uri, uriTemplate, version) on its
spec-required components. Passing null now throws
IllegalArgumentException at construction time instead of silently
producing invalid JSON via @JsonInclude(NON_ABSENT). This applies to
the JSON-RPC envelopes, lifecycle types, resource/prompt/tool requests
and results, sampling and elicitation, content records, root, complete,
logging and progress notifications, and the two CompleteReference
implementations. See MIGRATION-2.0.md for the full list.

Lenient wire deserialization
----------------------------
For each of those records (except JSONRPCResponse.JSONRPCError, which
still fails fast) a @JsonCreator static `fromJson` factory substitutes
a documented default for any absent required field — "" for strings,
[] for collections, {} for maps, 0 / 0.0 for numerics, INFO for
LoggingLevel, USER for SamplingMessage.role, ASSISTANT for
CreateMessageResult.role, CANCEL for ElicitResult.action, {values=[]}
for CompleteResult.completion — and logs a WARN naming the field and
the value used. Application code can still observe a malformed message
and react, but the SDK no longer halts the conversation.

Builder convention
------------------
Records that have a builder gain a required-first factory method
`builder(req1, req2, …)`; setters for required fields are removed from
the builder so it cannot be left in an invalid state. Existing no-arg
`builder()` factories and required-field setters are kept where source
compatibility demands it but are marked @Deprecated. New builders are
also added for several records that previously had none (ProgressNotification,
JSONRPCError, CompleteRequest, list/result types, content records, ...).

JSON-RPC envelope ergonomics
----------------------------
Previously every JSON-RPC envelope had to be constructed via the
canonical record constructor and the literal "2.0" string had to be
threaded through every call site, e.g.

    new JSONRPCRequest("2.0", "tools/call", id, params)
    new JSONRPCResponse("2.0", id, result, null)
    new JSONRPCResponse("2.0", id, null, new JSONRPCError(code, message, null))

Now:

    new JSONRPCRequest("tools/call", id, params)              // jsonrpc defaulted
    new JSONRPCNotification("notifications/initialized")       // params optional
    JSONRPCResponse.result(id, result)                        // factory
    JSONRPCResponse.error(id, new JSONRPCError(code, message)) // 2-arg error

JSONRPCResponse's compact constructor additionally enforces the
JSON-RPC invariant that exactly one of `result` / `error` is set —
previously the SDK could build envelopes that violated the protocol.

CompleteReference changes
-------------------------
- PromptReference.equals/hashCode now key on `name` only (previously
  derived from identifier()+type()). Two refs with the same name but
  different titles now compare equal — code using PromptReference as a
  map/set key should be audited.
- PromptReference's compact constructor pins `type` to "ref/prompt"
  and logs a WARN if the caller supplies a different non-null value.
  The legacy two-arg `PromptReference(String type, String name)`
  constructor remains @Deprecated.
- ResourceReference's record components are reduced from (type, uri)
  to (uri) — positional construction breaks. The legacy
  `ResourceReference(String type, String uri)` constructor stays
  @Deprecated and ignores `type`.
- CompleteReference.identifier() is @Deprecated and now returns null
  via a default method on the interface.

Tests / refactor
----------------
Conformance harness, integration tests, sample apps, and internal
callers were migrated to the new builder factories and convenience
constructors. No semantic changes to client/server runtime behaviour
beyond the McpSchema changes above.

Docs
----
- MIGRATION-2.0.md: required-field section now covers the broader
  record set and the lenient-deserialization behaviour; PromptReference
  WARN behaviour and ResourceReference component reduction are
  documented; the JSON-RPC envelope section is rewritten to compare the
  pre-2.0 surface with the new one.
- CONTRIBUTING.md: the "Evolving wire-serialized records" recipe is
  split into two cases — adding a new optional field (existing rules)
  and adding/maintaining a spec-required field (new rules covering
  Assert in compact constructor, @JsonCreator fromJson with defaults
  and WARN, required-first builder factory).

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
This commit is contained in:
Dariusz Jędrzejczyk 2026-04-20 17:20:38 +02:00 • committed by Christian Tzolov
commit 22a0efa577
30 changed files with 4258 additions and 1688 deletions

View file

@ -302,8 +302,9 @@ class LifecycleInitializer {
String latestVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
McpSchema.InitializeRequest initializeRequest = new McpSchema.InitializeRequest(latestVersion,
this.clientCapabilities, this.clientInfo);
McpSchema.InitializeRequest initializeRequest = McpSchema.InitializeRequest
.builder(latestVersion, this.clientCapabilities, this.clientInfo)
.build();
Mono<McpSchema.InitializeResult> result = mcpClientSession.sendRequest(McpSchema.METHOD_INITIALIZE,
initializeRequest, McpAsyncClient.INITIALIZE_RESULT_TYPE_REF);

View file

@ -538,7 +538,7 @@ public class McpAsyncClient {
List<Root> roots = this.roots.values().stream().toList();
return Mono.just(new McpSchema.ListRootsResult(roots));
return Mono.just(McpSchema.ListRootsResult.builder(roots).build());
};
}
@ -633,10 +633,10 @@ public class McpAsyncClient {
return this.listTools(McpSchema.FIRST_PAGE).expand(result -> {
String next = result.nextCursor();
return (next != null && !next.isEmpty()) ? this.listTools(next) : Mono.empty();
}).reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
allToolsResult.tools().addAll(result.tools());
return allToolsResult;
}).map(result -> new McpSchema.ListToolsResult(Collections.unmodifiableList(result.tools()), null));
}).reduce(new ArrayList<McpSchema.Tool>(), (accumulated, result) -> {
accumulated.addAll(result.tools());
return accumulated;
}).map(all -> McpSchema.ListToolsResult.builder(Collections.unmodifiableList(all)).build());
}
/**
@ -719,11 +719,11 @@ public class McpAsyncClient {
public Mono<McpSchema.ListResourcesResult> listResources() {
return this.listResources(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? this.listResources(result.nextCursor()) : Mono.empty())
.reduce(new McpSchema.ListResourcesResult(new ArrayList<>(), null), (allResourcesResult, result) -> {
allResourcesResult.resources().addAll(result.resources());
return allResourcesResult;
.reduce(new ArrayList<McpSchema.Resource>(), (accumulated, result) -> {
accumulated.addAll(result.resources());
return accumulated;
})
.map(result -> new McpSchema.ListResourcesResult(Collections.unmodifiableList(result.resources()), null));
.map(all -> McpSchema.ListResourcesResult.builder(Collections.unmodifiableList(all)).build());
}
/**
@ -774,7 +774,7 @@ public class McpAsyncClient {
* @see McpSchema.ReadResourceResult
*/
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.Resource resource) {
return this.readResource(new McpSchema.ReadResourceRequest(resource.uri()));
return this.readResource(McpSchema.ReadResourceRequest.builder(resource.uri()).build());
}
/**
@ -806,13 +806,11 @@ public class McpAsyncClient {
return this.listResourceTemplates(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? this.listResourceTemplates(result.nextCursor())
: Mono.empty())
.reduce(new McpSchema.ListResourceTemplatesResult(new ArrayList<>(), null),
(allResourceTemplatesResult, result) -> {
allResourceTemplatesResult.resourceTemplates().addAll(result.resourceTemplates());
return allResourceTemplatesResult;
})
.map(result -> new McpSchema.ListResourceTemplatesResult(
Collections.unmodifiableList(result.resourceTemplates()), null));
.reduce(new ArrayList<McpSchema.ResourceTemplate>(), (accumulated, result) -> {
accumulated.addAll(result.resourceTemplates());
return accumulated;
})
.map(all -> McpSchema.ListResourceTemplatesResult.builder(Collections.unmodifiableList(all)).build());
}
/**
@ -898,7 +896,7 @@ public class McpAsyncClient {
new TypeRef<>() {
});
return readResource(new McpSchema.ReadResourceRequest(resourcesUpdatedNotification.uri()))
return readResource(McpSchema.ReadResourceRequest.builder(resourcesUpdatedNotification.uri()).build())
.flatMap(readResourceResult -> Flux.fromIterable(resourcesUpdateConsumers)
.flatMap(consumer -> consumer.apply(readResourceResult.contents()))
.onErrorResume(error -> {
@ -927,11 +925,11 @@ public class McpAsyncClient {
public Mono<ListPromptsResult> listPrompts() {
return this.listPrompts(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? this.listPrompts(result.nextCursor()) : Mono.empty())
.reduce(new ListPromptsResult(new ArrayList<>(), null), (allPromptsResult, result) -> {
allPromptsResult.prompts().addAll(result.prompts());
return allPromptsResult;
.reduce(new ArrayList<McpSchema.Prompt>(), (accumulated, result) -> {
accumulated.addAll(result.prompts());
return accumulated;
})
.map(result -> new McpSchema.ListPromptsResult(Collections.unmodifiableList(result.prompts()), null));
.map(all -> McpSchema.ListPromptsResult.builder(Collections.unmodifiableList(all)).build());
}
/**

View file

@ -169,7 +169,7 @@ public interface McpClient {
private ClientCapabilities capabilities;
private Implementation clientInfo = new Implementation("Java SDK MCP Client", "0.15.0");
private Implementation clientInfo = Implementation.builder("Java SDK MCP Client", "0.15.0").build();
private final Map<String, Root> roots = new HashMap<>();
@ -525,7 +525,7 @@ public interface McpClient {
private ClientCapabilities capabilities;
private Implementation clientInfo = new Implementation("Java SDK MCP Client", "0.15.0");
private Implementation clientInfo = Implementation.builder("Java SDK MCP Client", "0.15.0").build();
private final Map<String, Root> roots = new HashMap<>();

View file

@ -106,7 +106,8 @@ class McpClientFeatures {
: new McpSchema.ClientCapabilities(null,
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
elicitationHandler != null ? new McpSchema.ClientCapabilities.Elicitation() : null);
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build()
: null);
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@ -256,7 +257,8 @@ class McpClientFeatures {
: new McpSchema.ClientCapabilities(null,
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
elicitationHandler != null ? new McpSchema.ClientCapabilities.Elicitation() : null);
elicitationHandler != null ? McpSchema.ClientCapabilities.Elicitation.builder().build()
: null);
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();

View file

@ -11,17 +11,15 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.modelcontextprotocol.util.Assert;
/**
* Server parameters for stdio client.
* Server parameters for stdio client. This is not a wire type; Jackson annotations are
* intentionally omitted.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public class ServerParameters {
// Environment variables to inherit by default
@ -32,13 +30,10 @@ public class ServerParameters {
"SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");
@JsonProperty("command")
private String command;
@JsonProperty("args")
private List<String> args = new ArrayList<>();
@JsonProperty("env")
private Map<String, String> env;
private ServerParameters(String command, List<String> args, Map<String, String> env) {

View file

@ -37,7 +37,7 @@ class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler {
.build());
}
return requestHandler.handle(transportContext, request.params())
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
.map(result -> McpSchema.JSONRPCResponse.result(request.id(), result))
.onErrorResume(t -> {
McpSchema.JSONRPCResponse.JSONRPCError error;
if (t instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
@ -45,9 +45,9 @@ class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler {
}
else {
error = new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
t.getMessage(), null);
t.getMessage());
}
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, error));
return Mono.just(McpSchema.JSONRPCResponse.error(request.id(), error));
});
}

View file

@ -414,7 +414,7 @@ public class McpAsyncServer {
String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
logger.warn(content);
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(content)))
.content(List.of(McpSchema.TextContent.builder(content).build()))
.isError(true)
.build();
}
@ -425,7 +425,7 @@ public class McpAsyncServer {
if (!validation.valid()) {
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
.content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build()))
.isError(true)
.build();
}
@ -438,7 +438,7 @@ public class McpAsyncServer {
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
.content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()))
.isError(result.isError())
.structuredContent(result.structuredContent())
.build();
@ -529,7 +529,7 @@ public class McpAsyncServer {
return (exchange, params) -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
return Mono.just(new McpSchema.ListToolsResult(tools, null));
return Mono.just(McpSchema.ListToolsResult.builder(tools).build());
};
}
@ -781,7 +781,7 @@ public class McpAsyncServer {
.stream()
.map(McpServerFeatures.AsyncResourceSpecification::resource)
.toList();
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
return Mono.just(McpSchema.ListResourcesResult.builder(resourceList).build());
};
}
@ -791,7 +791,7 @@ public class McpAsyncServer {
.stream()
.map(McpServerFeatures.AsyncResourceTemplateSpecification::resourceTemplate)
.toList();
return Mono.just(new McpSchema.ListResourceTemplatesResult(resourceList, null));
return Mono.just(McpSchema.ListResourceTemplatesResult.builder(resourceList).build());
};
}
@ -923,7 +923,7 @@ public class McpAsyncServer {
.map(McpServerFeatures.AsyncPromptSpecification::prompt)
.toList();
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
return Mono.just(McpSchema.ListPromptsResult.builder(promptList).build());
};
}

View file

@ -166,13 +166,13 @@ public class McpAsyncServerExchange {
return this.listRoots(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ?
this.listRoots(result.nextCursor()) : Mono.empty())
.reduce(new McpSchema.ListRootsResult(new ArrayList<>(), null),
.reduce(McpSchema.ListRootsResult.builder(new ArrayList<>()).build(),
(allRootsResult, result) -> {
allRootsResult.roots().addAll(result.roots());
return allRootsResult;
})
.map(result -> new McpSchema.ListRootsResult(Collections.unmodifiableList(result.roots()),
result.nextCursor()));
.map(result -> McpSchema.ListRootsResult.builder(Collections.unmodifiableList(result.roots()))
.nextCursor(result.nextCursor()).build());
// @formatter:on
}

View file

@ -66,9 +66,9 @@ import reactor.core.publisher.Mono;
* Example of creating a basic synchronous server: <pre>{@code
* McpServer.sync(transportProvider)
* .serverInfo("my-server", "1.0.0")
* .toolCall(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
* .toolCall(Tool.builder("calculator", schema).title("Performs calculations").build(),
* (exchange, request) -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + calculate(request.arguments()))))
* .content(List.of(McpSchema.TextContent.builder("Result: " + calculate(request.arguments())).build()))
* .isError(false)
* .build())
* .build();
@ -77,10 +77,10 @@ import reactor.core.publisher.Mono;
* Example of creating a basic asynchronous server: <pre>{@code
* McpServer.async(transportProvider)
* .serverInfo("my-server", "1.0.0")
* .toolCall(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
* .toolCall(Tool.builder("calculator", schema).title("Performs calculations").build(),
* (exchange, request) -> Mono.fromSupplier(() -> calculate(request.arguments()))
* .map(result -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + result)))
* .content(List.of(McpSchema.TextContent.builder("Result: " + result).build()))
* .isError(false)
* .build()))
* .build();
@ -97,7 +97,7 @@ import reactor.core.publisher.Mono;
* .tool(calculatorTool)
* .callTool((exchange, args) -> Mono.fromSupplier(() -> calculate(args.arguments()))
* .map(result -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + result)))
* .content(List.of(McpSchema.TextContent.builder("Result: " + result).build()))
* .isError(false)
* .build()))
*. .build(),
@ -105,7 +105,7 @@ import reactor.core.publisher.Mono;
* .tool((weatherTool)
* .callTool((exchange, args) -> Mono.fromSupplier(() -> getWeather(args.arguments()))
* .map(result -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Weather: " + result)))
* .content(List.of(McpSchema.TextContent.builder("Weather: " + result).build()))
* .isError(false)
* .build()))
* .build()
@ -145,7 +145,8 @@ import reactor.core.publisher.Mono;
*/
public interface McpServer {
McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("Java SDK MCP Server", "0.15.0");
McpSchema.Implementation DEFAULT_SERVER_INFO = McpSchema.Implementation.builder("Java SDK MCP Server", "0.15.0")
.build();
/**
* Starts building a synchronous MCP server that provides blocking operations.
@ -395,7 +396,7 @@ public interface McpServer {
public AsyncSpecification<S> serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version must not be null or empty");
this.serverInfo = new McpSchema.Implementation(name, version);
this.serverInfo = McpSchema.Implementation.builder(name, version).build();
return this;
}
@ -992,7 +993,7 @@ public interface McpServer {
public SyncSpecification<S> serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version must not be null or empty");
this.serverInfo = new McpSchema.Implementation(name, version);
this.serverInfo = McpSchema.Implementation.builder(name, version).build();
return this;
}
@ -1531,7 +1532,7 @@ public interface McpServer {
public StatelessAsyncSpecification serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version must not be null or empty");
this.serverInfo = new McpSchema.Implementation(name, version);
this.serverInfo = McpSchema.Implementation.builder(name, version).build();
return this;
}
@ -2028,7 +2029,7 @@ public interface McpServer {
public StatelessSyncSpecification serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version must not be null or empty");
this.serverInfo = new McpSchema.Implementation(name, version);
this.serverInfo = McpSchema.Implementation.builder(name, version).build();
return this;
}

View file

@ -77,10 +77,12 @@ public class McpServerFeatures {
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of();
@ -192,10 +194,12 @@ public class McpServerFeatures {
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>();
@ -487,20 +491,19 @@ public class McpServerFeatures {
*
* <pre>{@code
* McpServerFeatures.SyncToolSpecification.builder()
* .tool(Tool.builder()
* .name("calculator")
* .tool(Tool.builder("calculator",
* Map.of("type", "object", "properties",
* Map.of("expression", Map.of("type", "string")),
* "required", List.of("expression")))
* .title("Performs mathematical calculations")
* .inputSchema(new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING))
* .build()
* .build())
* .toolHandler((exchange, req) -> {
* String expr = (String) req.arguments().get("expression");
* return CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + evaluate(expr))))
* .content(List.of(McpSchema.TextContent.builder("Result: " + evaluate(expr)).build()))
* .isError(false)
* .build();
* }))
* })
* .build();
* }</pre>
*

View file

@ -284,7 +284,7 @@ public class McpStatelessAsyncServer {
String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
logger.warn(content);
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(content)))
.content(List.of(McpSchema.TextContent.builder(content).build()))
.isError(true)
.build();
}
@ -295,7 +295,7 @@ public class McpStatelessAsyncServer {
if (!validation.valid()) {
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
.content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build()))
.isError(true)
.build();
}
@ -308,7 +308,7 @@ public class McpStatelessAsyncServer {
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
.content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()))
.isError(result.isError())
.structuredContent(result.structuredContent())
.build();
@ -393,7 +393,7 @@ public class McpStatelessAsyncServer {
List<Tool> tools = this.tools.stream()
.map(McpStatelessServerFeatures.AsyncToolSpecification::tool)
.toList();
return Mono.just(new McpSchema.ListToolsResult(tools, null));
return Mono.just(McpSchema.ListToolsResult.builder(tools).build());
};
}
@ -557,7 +557,7 @@ public class McpStatelessAsyncServer {
.stream()
.map(McpStatelessServerFeatures.AsyncResourceSpecification::resource)
.toList();
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
return Mono.just(McpSchema.ListResourcesResult.builder(resourceList).build());
};
}
@ -567,7 +567,7 @@ public class McpStatelessAsyncServer {
.stream()
.map(AsyncResourceTemplateSpecification::resourceTemplate)
.toList();
return Mono.just(new McpSchema.ListResourceTemplatesResult(resourceList, null));
return Mono.just(McpSchema.ListResourceTemplatesResult.builder(resourceList).build());
};
}
@ -687,7 +687,7 @@ public class McpStatelessAsyncServer {
.map(McpStatelessServerFeatures.AsyncPromptSpecification::prompt)
.toList();
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
return Mono.just(McpSchema.ListPromptsResult.builder(promptList).build());
};
}

View file

@ -71,10 +71,12 @@ public class McpStatelessServerFeatures {
: new McpSchema.ServerCapabilities(null, // completions
null, // experimental
null, // currently statless server doesn't support set logging
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of();
@ -172,10 +174,12 @@ public class McpStatelessServerFeatures {
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>();

View file

@ -462,8 +462,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
response.setHeader(HttpHeaders.MCP_SESSION_ID, init.session().getId());
response.setStatus(HttpServletResponse.SC_OK);
String jsonResponse = jsonMapper.writeValueAsString(new McpSchema.JSONRPCResponse(
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null));
String jsonResponse = jsonMapper
.writeValueAsString(McpSchema.JSONRPCResponse.result(jsonrpcRequest.id(), initResult));
PrintWriter writer = response.getWriter();
writer.write(jsonResponse);

View file

@ -154,12 +154,10 @@ public class McpClientSession implements McpSession {
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = (error instanceof McpError mcpError
&& mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
// TODO: add error message through the data field
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), McpError.aggregateExceptionMessages(error));
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
jsonRpcError);
var errorResponse = McpSchema.JSONRPCResponse.error(request.id(), jsonRpcError);
return Mono.just(errorResponse);
}).flatMap(this.transport::sendMessage).onErrorComplete(t -> {
logger.warn("Issue sending response to the client, ", t);
@ -188,13 +186,13 @@ public class McpClientSession implements McpSession {
var handler = this.requestHandlers.get(request.method());
if (handler == null) {
MethodNotFoundError error = getMethodNotFoundError(request.method());
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
error.message(), error.data())));
return Mono
.just(McpSchema.JSONRPCResponse.error(request.id(), new McpSchema.JSONRPCResponse.JSONRPCError(
McpSchema.ErrorCodes.METHOD_NOT_FOUND, error.message(), error.data())));
}
return handler.handle(request.params())
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null));
.map(result -> McpSchema.JSONRPCResponse.result(request.id(), result));
});
}
@ -251,8 +249,7 @@ public class McpClientSession implements McpSession {
return Mono.deferContextual(ctx -> Mono.<McpSchema.JSONRPCResponse>create(pendingResponseSink -> {
logger.debug("Sending message for method {}", method);
this.pendingResponses.put(requestId, pendingResponseSink);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
requestId, requestParams);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(method, requestId, requestParams);
this.transport.sendMessage(jsonrpcRequest).contextWrite(ctx).subscribe(v -> {
}, error -> {
this.pendingResponses.remove(requestId);
@ -282,8 +279,7 @@ public class McpClientSession implements McpSession {
*/
@Override
public Mono<Void> sendNotification(String method, Object params) {
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
method, params);
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(method, params);
return this.transport.sendMessage(jsonrpcNotification);
}

View file

@ -153,8 +153,7 @@ public class McpServerSession implements McpLoggableSession {
return Mono.<McpSchema.JSONRPCResponse>create(sink -> {
this.pendingResponses.put(requestId, sink);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
requestId, requestParams);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(method, requestId, requestParams);
this.transport.sendMessage(jsonrpcRequest).subscribe(v -> {
}, error -> {
this.pendingResponses.remove(requestId);
@ -177,8 +176,7 @@ public class McpServerSession implements McpLoggableSession {
@Override
public Mono<Void> sendNotification(String method, Object params) {
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
method, params);
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(method, params);
return this.transport.sendMessage(jsonrpcNotification);
}
@ -223,8 +221,7 @@ public class McpServerSession implements McpLoggableSession {
&& mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), McpError.aggregateExceptionMessages(error));
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
jsonRpcError);
var errorResponse = McpSchema.JSONRPCResponse.error(request.id(), jsonRpcError);
// TODO: Should the error go to SSE or back as POST return?
return this.transport.sendMessage(errorResponse).then(Mono.empty());
}).flatMap(this.transport::sendMessage);
@ -270,24 +267,21 @@ public class McpServerSession implements McpLoggableSession {
var handler = this.requestHandlers.get(request.method());
if (handler == null) {
MethodNotFoundError error = getMethodNotFoundError(request.method());
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
error.message(), error.data())));
return Mono
.just(McpSchema.JSONRPCResponse.error(request.id(), new McpSchema.JSONRPCResponse.JSONRPCError(
McpSchema.ErrorCodes.METHOD_NOT_FOUND, error.message(), error.data())));
}
resultMono = this.exchangeSink.asMono()
.flatMap(exchange -> handler.handle(copyExchange(exchange, transportContext), request.params()));
}
return resultMono
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
return resultMono.map(result -> McpSchema.JSONRPCResponse.result(request.id(), result))
.onErrorResume(error -> {
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = (error instanceof McpError mcpError
&& mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
// TODO: add error message through the data field
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), McpError.aggregateExceptionMessages(error));
return Mono.just(
new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, jsonRpcError));
return Mono.just(McpSchema.JSONRPCResponse.error(request.id(), jsonRpcError));
});
});
}

View file

@ -190,24 +190,21 @@ public class McpStreamableServerSession implements McpLoggableSession {
// (sink)
if (requestHandler == null) {
MethodNotFoundError error = getMethodNotFoundError(jsonrpcRequest.method());
return transport
.sendMessage(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
error.message(), error.data())));
return transport.sendMessage(
McpSchema.JSONRPCResponse.error(jsonrpcRequest.id(), new McpSchema.JSONRPCResponse.JSONRPCError(
McpSchema.ErrorCodes.METHOD_NOT_FOUND, error.message(), error.data())));
}
return requestHandler
.handle(new McpAsyncServerExchange(this.id, stream, clientCapabilities.get(), clientInfo.get(),
transportContext), jsonrpcRequest.params())
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), result,
null))
.map(result -> McpSchema.JSONRPCResponse.result(jsonrpcRequest.id(), result))
.onErrorResume(e -> {
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = (e instanceof McpError mcpError
&& mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
e.getMessage(), McpError.aggregateExceptionMessages(e));
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(),
null, jsonRpcError);
var errorResponse = McpSchema.JSONRPCResponse.error(jsonrpcRequest.id(), jsonRpcError);
return Mono.just(errorResponse);
})
.flatMap(transport::sendMessage)
@ -381,8 +378,8 @@ public class McpStreamableServerSession implements McpLoggableSession {
return Mono.<McpSchema.JSONRPCResponse>create(sink -> {
this.pendingResponses.put(requestId, sink);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
method, requestId, requestParams);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(method, requestId,
requestParams);
String messageId = this.uuidGenerator.get();
// TODO: store message in history
this.transport.sendMessage(jsonrpcRequest, messageId).subscribe(v -> {
@ -407,8 +404,7 @@ public class McpStreamableServerSession implements McpLoggableSession {
@Override
public Mono<Void> sendNotification(String method, Object params) {
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(
McpSchema.JSONRPC_VERSION, method, params);
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(method, params);
String messageId = this.uuidGenerator.get();
// TODO: store message in history
return this.transport.sendMessage(jsonrpcNotification, messageId);

View file

@ -36,7 +36,7 @@ public final class ToolInputValidator {
*/
public static CallToolResult validate(McpSchema.Tool tool, Map<String, Object> arguments,
boolean validateToolInputs, JsonSchemaValidator validator) {
if (!validateToolInputs || tool.inputSchema() == null || validator == null) {
if (!validateToolInputs || tool.inputSchema() == null || tool.inputSchema().isEmpty() || validator == null) {
return null;
}
Map<String, Object> args = arguments != null ? arguments : Map.of();
@ -44,7 +44,7 @@ public final class ToolInputValidator {
if (!validation.valid()) {
logger.warn("Tool '{}' input validation failed: {}", tool.name(), validation.errorMessage());
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
.content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build()))
.isError(true)
.build();
}

View file

@ -43,13 +43,16 @@ class LifecycleInitializerPostInitializationHookTests {
private static final McpSchema.ClientCapabilities CLIENT_CAPABILITIES = McpSchema.ClientCapabilities.builder()
.build();
private static final McpSchema.Implementation CLIENT_INFO = new McpSchema.Implementation("test-client", "1.0.0");
private static final McpSchema.Implementation CLIENT_INFO = McpSchema.Implementation.builder("test-client", "1.0.0")
.build();
private static final List<String> PROTOCOL_VERSIONS = List.of("1.0.0", "2.0.0");
private static final McpSchema.InitializeResult MOCK_INIT_RESULT = new McpSchema.InitializeResult("2.0.0",
McpSchema.ServerCapabilities.builder().build(), new McpSchema.Implementation("test-server", "1.0.0"),
"Test instructions");
private static final McpSchema.InitializeResult MOCK_INIT_RESULT = McpSchema.InitializeResult
.builder("2.0.0", McpSchema.ServerCapabilities.builder().build(),
McpSchema.Implementation.builder("test-server", "1.0.0").build())
.instructions("Test instructions")
.build();
@Mock
private McpClientSession mockClientSession;

View file

@ -44,13 +44,16 @@ class LifecycleInitializerTests {
private static final McpSchema.ClientCapabilities CLIENT_CAPABILITIES = McpSchema.ClientCapabilities.builder()
.build();
private static final McpSchema.Implementation CLIENT_INFO = new McpSchema.Implementation("test-client", "1.0.0");
private static final McpSchema.Implementation CLIENT_INFO = McpSchema.Implementation.builder("test-client", "1.0.0")
.build();
private static final List<String> PROTOCOL_VERSIONS = List.of("1.0.0", "2.0.0");
private static final McpSchema.InitializeResult MOCK_INIT_RESULT = new McpSchema.InitializeResult("2.0.0",
McpSchema.ServerCapabilities.builder().build(), new McpSchema.Implementation("test-server", "1.0.0"),
"Test instructions");
private static final McpSchema.InitializeResult MOCK_INIT_RESULT = McpSchema.InitializeResult
.builder("2.0.0", McpSchema.ServerCapabilities.builder().build(),
McpSchema.Implementation.builder("test-server", "1.0.0").build())
.instructions("Test instructions")
.build();
@Mock
private McpClientSession mockClientSession;
@ -148,10 +151,12 @@ class LifecycleInitializerTests {
@Test
void shouldFailForUnsupportedProtocolVersion() {
McpSchema.InitializeResult unsupportedResult = new McpSchema.InitializeResult("999.0.0", // Unsupported
// version
McpSchema.ServerCapabilities.builder().build(), new McpSchema.Implementation("test-server", "1.0.0"),
"Test instructions");
McpSchema.InitializeResult unsupportedResult = McpSchema.InitializeResult.builder("999.0.0", // Unsupported
// version
McpSchema.ServerCapabilities.builder().build(),
McpSchema.Implementation.builder("test-server", "1.0.0").build())
.instructions("Test instructions")
.build();
when(mockClientSession.sendRequest(eq(McpSchema.METHOD_INITIALIZE), any(), any()))
.thenReturn(Mono.just(unsupportedResult));
@ -342,8 +347,11 @@ class LifecycleInitializerTests {
when(mockClientSession.sendRequest(eq(McpSchema.METHOD_INITIALIZE), any(), any())).thenAnswer(invocation -> {
capturedRequest.set((McpSchema.InitializeRequest) invocation.getArgument(1));
return Mono.just(new McpSchema.InitializeResult("4.0.0", McpSchema.ServerCapabilities.builder().build(),
new McpSchema.Implementation("test-server", "1.0.0"), "Test instructions"));
return Mono.just(McpSchema.InitializeResult
.builder("4.0.0", McpSchema.ServerCapabilities.builder().build(),
McpSchema.Implementation.builder("test-server", "1.0.0").build())
.instructions("Test instructions")
.build());
});
StepVerifier.create(initializer.withInitialization("test", init -> Mono.just(init.initializeResult())))

Some files were not shown because too many files have changed in this diff Show more