Update documentation and migration notes for v2 (#1024)

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
This commit is contained in:
Dariusz Jędrzejczyk 2026-06-11 11:38:19 +02:00 • committed by GitHub
commit 275633775c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 273 additions and 191 deletions

View file

@ -1,81 +1,19 @@
# Migration Guide — 2.0
This document covers breaking and behavioural changes introduced in the 2.0 release of the MCP Java SDK.
This guide covers the breaking and behavioural changes introduced in the 2.0 release of the MCP Java SDK, relative to 1.x, and how to update existing code.
The changes fall into these areas:
- [Schema construction and required fields](#schema-construction-and-required-fields) — non-null enforcement and the builder API.
- [Schema type and shape changes](#schema-type-and-shape-changes) — record component and type changes in `McpSchema`.
- [JSON serialization behaviour](#json-serialization-behaviour) — wire-format changes.
- [Server-side validation](#server-side-validation) — runtime validation of tool arguments and embedded schemas.
- [Transport changes](#transport-changes) — removed methods and the SSE deprecation.
- [New features](#new-features) — additive, backward-compatible capabilities.
---
## Jackson / JSON serialization changes
### Sealed interfaces removed
The following interfaces were `sealed` in 1.x and are now plain interfaces in 2.0:
- `McpSchema.JSONRPCMessage`
- `McpSchema.Request`
- `McpSchema.Result`
- `McpSchema.Notification`
- `McpSchema.ResourceContents`
- `McpSchema.CompleteReference`
- `McpSchema.Content`
**Impact:** Exhaustive `switch` expressions or `switch` statements that relied on the sealed hierarchy for completeness checking must add a `default` branch. The compiler will no longer reject switches that omit one of the known subtypes.
### `CompleteReference` now carries `@JsonTypeInfo`
`CompleteReference` (and its implementations `PromptReference` and `ResourceReference`) is now annotated with `@JsonTypeInfo(use = NAME, include = EXISTING_PROPERTY, property = "type", visible = true)`. Jackson will automatically dispatch to the correct subtype based on the `"type"` field in the JSON without any hand-written map-walking code.
**Action:** Remove any custom code that manually inspected the `"type"` field of a completion reference map and instantiated `PromptReference` / `ResourceReference` by hand. A plain `mapper.readValue(json, CompleteRequest.class)` or `mapper.convertValue(paramsMap, CompleteRequest.class)` is sufficient.
### `Prompt` canonical constructor no longer coerces `null` arguments
In 1.x, `new Prompt(name, description, null)` silently stored an empty list for `arguments`. In 2.0 it stores `null`.
**Action:**
- Code that expected `prompt.arguments()` to return an empty list when not provided will now receive `null`. Add a null-check or use the new `Prompt.withDefaults(name, description, arguments)` factory, which preserves the old behaviour by coercing `null` to `[]`.
- On the wire, a prompt without an `arguments` field deserializes with `arguments == null` (it is not coerced to an empty list).
### `CompleteCompletion` optional fields omitted when null
`CompleteResult.CompleteCompletion.total` and `CompleteCompletion.hasMore` are now omitted from serialized JSON when they are `null` (previously they were always emitted). Deserializers that required these fields to be present in every response must be updated to treat their absence as `null`.
### `CompleteCompletion.values` is mandatory in the Java API
The compact constructor for `CompleteCompletion` asserts that `values` is not `null`. Code that constructed a completion result with a null `values` list will now fail at runtime.
**Action:** Always pass a non-null list (for example `List.of()` when there are no suggestions).
### `LoggingLevel` deserialization is lenient
`LoggingLevel` now uses a `@JsonCreator` factory (`fromValue`) so that JSON string values deserialize in a case-insensitive way. **Unrecognized level strings deserialize to `null`** instead of causing deserialization to fail.
**Impact:** `SetLevelRequest`, `LoggingMessageNotification`, and any other type that embeds `LoggingLevel` can observe a `null` level when the wire value is unknown or misspelled. Downstream code must null-check or validate before use.
### `Content.type()` is ignored for Jackson serialization
The `Content` interface still exposes `type()` as a convenience for Java callers, but the method is annotated with `@JsonIgnore` so Jackson does not treat it as a duplicate `"type"` property alongside `@JsonTypeInfo` on the interface.
**Impact:** Custom serializers or `ObjectMapper` configuration that relied on serializing `Content` through the default `type()` accessor alone should use the concrete content records (each of which carries a real `"type"` property) or the polymorphic setup on `Content`.
### `ServerParameters` no longer carries Jackson annotations
`ServerParameters` (in `client/transport`) has had its `@JsonProperty` and `@JsonInclude` annotations removed. It was never a wire type and is not serialized to JSON in normal SDK usage. If your code serialized or deserialized `ServerParameters` using Jackson, switch to a plain map or a dedicated DTO.
### Record annotation sweep
Wire-oriented `public record` types in `McpSchema` consistently use `@JsonInclude(JsonInclude.Include.NON_ABSENT)` (or equivalent per-type configuration) and `@JsonIgnoreProperties(ignoreUnknown = true)`. Nested capability objects under `ClientCapabilities` / `ServerCapabilities` (for example `Sampling`, `Elicitation`, `CompletionCapabilities`, `LoggingCapabilities`, prompt/resource/tool capability records) also ignore unknown JSON properties. This means:
- **Unknown fields** in incoming JSON are silently ignored, improving forward compatibility with newer server or client versions.
- **Absent optional properties** are omitted from outgoing JSON where `NON_ABSENT` applies, and optional Java components deserialize as `null` when missing on the wire.
### `Tool.inputSchema` is `Map<String, Object>`, not `JsonSchema`
The `Tool` record now models `inputSchema` (and `outputSchema`) as arbitrary JSON Schema objects as `Map<String, Object>`, so dialect-specific keywords (`$ref`, `unevaluatedProperties`, vendor extensions, and so on) round-trip without being trimmed by a narrow `JsonSchema` record.
**Impact:**
- Java code that used `Tool.inputSchema()` as a `JsonSchema` must switch to `Map<String, Object>` (or copy into your own schema wrapper).
- `Tool.Builder.inputSchema(JsonSchema)` remains as a **deprecated** helper that maps the old record into a map; prefer `inputSchema(Map)` or `inputSchema(McpJsonMapper, String)`.
## Schema construction and required fields
### Required MCP spec fields are enforced at construction time
@ -93,30 +31,22 @@ This applies to (non-exhaustive):
**Action:** Audit any code that constructs these records with potentially-null values and provide valid, non-null arguments.
**Wire deserialization is lenient.** Records expose a `@JsonCreator fromJson` factory that substitutes safe defaults (e.g. `[]`, `""`, `0`, `INFO`, `Action.CANCEL`) for any absent required field and logs a `WARN` naming the field and the substituted value. `JSONRPCResponse.JSONRPCError` is excluded — malformed JSON-RPC error envelopes still fail immediately.
**Wire deserialization stays lenient.** Records expose a `@JsonCreator fromJson` factory that substitutes safe defaults (e.g. `[]`, `""`, `0`, `INFO`, `Action.CANCEL`) for any absent required field and logs a `WARN` naming the field and the substituted value. `JSONRPCResponse.JSONRPCError` is excluded — malformed JSON-RPC error envelopes still fail immediately.
**Note:** `LoggingMessageNotification`/`SetLevelRequest` default a *missing* `level` to `INFO`, but an *unrecognized* level string still deserializes to `null` (see the `LoggingLevel` section above) and will then fail the canonical constructor. Ensure clients and servers send only recognized level strings.
**Note:** `LoggingMessageNotification` / `SetLevelRequest` default a *missing* `level` to `INFO`, but an *unrecognized* level string still deserializes to `null` (see [`LoggingLevel` deserialization is lenient](#logginglevel-deserialization-is-lenient)) and will then fail the canonical constructor. Ensure clients and servers send only recognized level strings.
### `PromptReference` discriminator pinning and equality
### `Prompt` no longer coerces `null` arguments
`PromptReference` keeps its `(type, name, title)` record components, so positional construction from 1.x still compiles. Two behavioural changes:
In 1.x, `new Prompt(name, description, null)` silently stored an empty list for `arguments`. In 2.0 it stores `null`.
- The compact constructor pins `type` to `ref/prompt`. Any non-null value other than `ref/prompt` is replaced with `ref/prompt` and a `WARN` is logged. The legacy two-arg `PromptReference(String type, String name)` constructor remains `@Deprecated` and routes through the canonical constructor, so it triggers the same WARN.
- `equals`/`hashCode` now consider `name` only (title and type are ignored). Two refs with the same name but different titles compare equal.
**Action:**
**Action:** Audit any code that used `PromptReference` as a map key or in a `Set` — equality semantics changed. If your code constructed instances with a custom `type` string for testing, switch to `PromptReference.builder(name)` (or `new PromptReference(name)`); the WARN tells you which call sites still pass the discriminator.
`CompleteReference.identifier()` is `@Deprecated` and now returns `null` via a default method on the interface.
### `ResourceReference` record component reduced
Components changed from `(type, uri)` to `(uri)`. Positional construction with two arguments breaks. The legacy `ResourceReference(String type, String uri)` constructor stays `@Deprecated`; it ignores `type` and logs a `WARN`. Use `new ResourceReference(uri)` or `ResourceReference.builder(uri)`. The `type()` accessor still returns `ref/resource` and Jackson serializes it via `@JsonProperty("type")` on the accessor.
- Code that expected `prompt.arguments()` to return an empty list when not provided will now receive `null`. Add a null-check.
- On the wire, a prompt without an `arguments` field deserializes with `arguments == null` (it is not coerced to an empty list).
### Builder API: required-first factories; old setters/no-arg builders deprecated
Most records that have a builder have gained a required-first factory method (`builder(req1, req2, …)`) and the corresponding setters for required fields are removed from the builder. The old no-arg `builder()` factory and public no-arg `Builder()` constructor are kept but `@Deprecated` where they would allow constructing a builder without required state.
Examples:
Most records that have a builder gained a required-first factory method (`builder(req1, req2, …)`). The old no-arg `builder()` factory, the public no-arg `Builder()` constructor, and the setters for the now-required fields are kept but `@Deprecated`. They still compile, so 1.x code keeps working with deprecation warnings; migrate to the required-first factories to clear them.
| Type | Old (deprecated) | New |
|------|-----------------|-----|
@ -124,7 +54,7 @@ Examples:
| `ResourceTemplate` | `ResourceTemplate.builder().uriTemplate(u).name(n)…` | `ResourceTemplate.builder(uriTemplate, name)…` |
| `Implementation` | `new Implementation(name, version)` | `Implementation.builder(name, version)…` |
| `InitializeRequest` / `InitializeResult` | `… .builder()…` | `… .builder(protocolVersion, capabilities, clientInfo/serverInfo)` |
| `Tool` | `Tool.builder().name(n)…` | `Tool.builder(name)…` |
| `Tool` | `Tool.builder().name(n).inputSchema(s)…` | `Tool.builder(name, inputSchemaMap)…` or `Tool.builder(name, jsonMapper, inputSchemaJson)…` |
| `Prompt` / `PromptArgument` / `GetPromptRequest` | `… .builder().name(n)…` | `… .builder(name)…` |
| `PromptMessage` / `SamplingMessage` | `… .builder().role(r).content(c)…` | `… .builder(role, content)…` |
| `CreateMessageRequest` | `… .builder().messages(m).maxTokens(n)…` | `… .builder(messages, maxTokens)…` |
@ -133,15 +63,105 @@ Examples:
| `ListResourcesResult` / `ListResourceTemplatesResult` / `ListPromptsResult` / `ListToolsResult` / `ListRootsResult` | `… .builder()…` | `… .builder(items)…` |
| `ReadResourceRequest` / `SubscribeRequest` / `UnsubscribeRequest` / `ResourcesUpdatedNotification` / `Root` | n/a | `… .builder(uri)…` |
| `ReadResourceResult` | n/a | `ReadResourceResult.builder(contents)…` |
| `TextResourceContents` / `BlobResourceContents` | n/a | `… .builder(uri, text|blob)…` |
| `GetPromptResult` | `new GetPromptResult(description, messages)` | `GetPromptResult.builder(messages).description(d)…` |
| `TextResourceContents` / `BlobResourceContents` | n/a | `… .builder(uri, text\|blob)…` |
| `TextContent` / `ImageContent` / `AudioContent` / `EmbeddedResource` | n/a | `… .builder(text \| data, mimeType \| resource)…` |
| `CallToolResult` | unchanged | also: required-first content set via builder constructor remains optional |
| `ProgressNotification` | n/a | `ProgressNotification.builder(progressToken, progress)` |
| `JSONRPCResponse.JSONRPCError` | n/a | `JSONRPCError.builder(code, message)` |
| `CompleteRequest` | n/a | `CompleteRequest.builder(ref, argument)` |
| `Annotations` | n/a | `Annotations.builder()` |
| Capabilities (`Sampling`, `Elicitation`, `Roots`, `LoggingCapabilities`, `CompletionCapabilities`, prompt/resource/tool capabilities) | n/a | `… .builder()…` |
---
## Schema type and shape changes
### `Tool.inputSchema` is `Map<String, Object>`, not `JsonSchema`
The `Tool` record now models `inputSchema` (and `outputSchema`) as arbitrary JSON Schema objects of type `Map<String, Object>`, so dialect-specific keywords (`$ref`, `unevaluatedProperties`, vendor extensions, and so on) round-trip without being trimmed by a narrow `JsonSchema` record.
**Action:**
- Java code that used `Tool.inputSchema()` as a `JsonSchema` must switch to `Map<String, Object>` (or copy into your own schema wrapper).
- `Tool.Builder.inputSchema(JsonSchema)` remains as a **deprecated** helper that maps the old record into a map; prefer `inputSchema(Map)` or `inputSchema(McpJsonMapper, String)`.
### Sealed interfaces removed
The following interfaces were `sealed` in 1.x and are now plain interfaces in 2.0:
- `McpSchema.JSONRPCMessage`
- `McpSchema.Request`
- `McpSchema.Result`
- `McpSchema.Notification`
- `McpSchema.ResourceContents`
- `McpSchema.CompleteReference`
- `McpSchema.Content`
**Impact:** Exhaustive `switch` expressions or statements that relied on the sealed hierarchy for completeness checking must add a `default` branch. The compiler will no longer reject switches that omit one of the known subtypes.
### `CompleteReference` polymorphic deserialization
`CompleteReference` (and its implementations `PromptReference` and `ResourceReference`) is now annotated with `@JsonTypeInfo(use = NAME, include = EXISTING_PROPERTY, property = "type", visible = true)`. Jackson dispatches to the correct subtype based on the `"type"` field automatically.
**Action:** Remove any custom code that manually inspected the `"type"` field of a completion reference map and instantiated `PromptReference` / `ResourceReference` by hand. A plain `mapper.readValue(json, CompleteRequest.class)` or `mapper.convertValue(paramsMap, CompleteRequest.class)` is sufficient.
`CompleteReference.identifier()` is `@Deprecated` and now returns `null` via a default method on the interface.
### `PromptReference` discriminator pinning and equality
`PromptReference` keeps its `(type, name, title)` record components, so positional construction from 1.x still compiles, with two behavioural changes:
- The compact constructor pins `type` to `ref/prompt`. Any non-null value other than `ref/prompt` is replaced and a `WARN` is logged. The legacy two-arg `PromptReference(String type, String name)` constructor remains `@Deprecated` and routes through the canonical constructor, so it triggers the same WARN.
- `equals`/`hashCode` now consider `name` only (title and type are ignored). Two refs with the same name but different titles compare equal.
**Action:** Audit any code that used `PromptReference` as a map key or in a `Set` — equality semantics changed. If you constructed instances with a custom `type` string, switch to `PromptReference.builder(name)` (or `new PromptReference(name)`); the WARN identifies the call sites still passing a discriminator.
### `ResourceReference` record component reduced
Components changed from `(type, uri)` to `(uri)`. Positional construction with two arguments breaks. The legacy `ResourceReference(String type, String uri)` constructor stays `@Deprecated`; it ignores `type` and logs a `WARN`. Use `new ResourceReference(uri)` or `ResourceReference.builder(uri)`. The `type()` accessor still returns `ref/resource` and Jackson serializes it via `@JsonProperty("type")` on the accessor.
### `ElicitRequest` is now an interface
To support URL-mode elicitation (see [New features](#new-features)), the elicitation request type was split:
- `ElicitRequest` changed from a `record` to an `interface`.
- The original form-based request record is now `ElicitFormRequest`.
- The `McpClient` builder `elicitation(...)` methods now accept a handler over `ElicitFormRequest` instead of `ElicitRequest`.
**Action:** Replace references to the old `ElicitRequest` record (construction, `instanceof`, handler signatures) with `ElicitFormRequest`. Code that only referred to `ElicitRequest` as a type continues to compile against the new interface.
### `ServerParameters` no longer carries Jackson annotations
`ServerParameters` (in `client/transport`) has had its `@JsonProperty` and `@JsonInclude` annotations removed. It was never a wire type and is not serialized to JSON in normal SDK usage. If your code serialized or deserialized `ServerParameters` using Jackson, switch to a plain map or a dedicated DTO.
---
## JSON serialization behaviour
### Unknown JSON fields are ignored
Wire-oriented `public record` types in `McpSchema` consistently use `@JsonInclude(JsonInclude.Include.NON_ABSENT)` and `@JsonIgnoreProperties(ignoreUnknown = true)`. Nested capability objects under `ClientCapabilities` / `ServerCapabilities` (for example `Sampling`, `Elicitation`, `CompletionCapabilities`, `LoggingCapabilities`, and the prompt/resource/tool capability records) also ignore unknown JSON properties. As a result:
- **Unknown fields** in incoming JSON are silently ignored, improving forward compatibility with newer server or client versions.
- **Absent optional properties** are omitted from outgoing JSON where `NON_ABSENT` applies, and optional Java components deserialize as `null` when missing on the wire.
### `CompleteCompletion` field handling
- `CompleteResult.CompleteCompletion.total` and `CompleteCompletion.hasMore` are now omitted from serialized JSON when `null` (previously they were always emitted). Deserializers that required these fields to be present must treat their absence as `null`.
- The compact constructor asserts that `values` is not `null`. **Action:** always pass a non-null list (for example `List.of()` when there are no suggestions).
### `LoggingLevel` deserialization is lenient
`LoggingLevel` now uses a `@JsonCreator` factory (`fromValue`) so JSON string values deserialize case-insensitively. **Unrecognized level strings deserialize to `null`** instead of failing.
**Impact:** `SetLevelRequest`, `LoggingMessageNotification`, and any other type embedding `LoggingLevel` can observe a `null` level when the wire value is unknown or misspelled. Downstream code must null-check or validate before use.
### `Content.type()` is ignored for Jackson serialization
The `Content` interface still exposes `type()` as a convenience for Java callers, but the method is annotated with `@JsonIgnore` so Jackson does not treat it as a duplicate `"type"` property alongside `@JsonTypeInfo` on the interface.
**Impact:** Custom serializers or `ObjectMapper` configuration that relied on serializing `Content` through the `type()` accessor alone should use the concrete content records (each of which carries a real `"type"` property) or the polymorphic setup on `Content`.
### JSON-RPC envelope ergonomics
In 1.x, every envelope was constructed via the canonical record constructor and the literal `"2.0"` `jsonrpc` string had to be threaded through every call site:
@ -164,12 +184,58 @@ JSONRPCResponse.result(id, result);
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.
`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. The 1.x canonical 4-arg constructors continue to compile.
The 1.x canonical 4-arg constructors continue to compile.
---
### Optional JSON Schema validation on `tools/call` (server)
## Server-side validation
### Optional JSON Schema validation on `tools/call`
When a `JsonSchemaValidator` is available (including the default from `McpJsonDefaults.getSchemaValidator()` when you do not configure one explicitly) and `validateToolInputs` is left at its default of `true`, the server validates incoming tool arguments against `tool.inputSchema()` before invoking the tool. Failed validation produces a `CallToolResult` with `isError` set and a textual error in the content.
**Action:** Ensure `inputSchema` maps are valid for your validator, tighten client arguments, or disable validation with `validateToolInputs(false)` on the server builder if you must preserve pre-2.0 behaviour.
### Embedded JSON Schemas are validated against 2020-12 (SEP-1613)
The JSON Schema documents that MCP embeds — `Tool.inputSchema`, `Tool.outputSchema`, and `ElicitRequest.requestedSchema` — are now validated against the JSON Schema 2020-12 meta-schema by default. Servers reject malformed schemas at **build time** (`McpServer.build()`) and at **runtime** (`addTool()`) with an `IllegalArgumentException` that names the offending field and references SEP-1613. Elicitation requests whose `requestedSchema` violates the meta-schema are rejected before being sent to the client.
Schemas that explicitly declare a different dialect via `$schema` are accepted without meta-schema validation — 2020-12 is the default, not the only permitted dialect.
**Action:** Make embedded schemas valid 2020-12 documents, or set an explicit `$schema` to opt into a different dialect.
---
## Transport changes
### `customizeRequest()` removed from the HttpClient transport builders
The deprecated `Builder.customizeRequest(Consumer<HttpRequest.Builder>)` method on `HttpClientSseClientTransport` and `HttpClientStreamableHttpTransport` has been removed.
**Action:** Use `requestBuilder(HttpRequest.Builder)` for static request setup, or `httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)` for per-request customization.
### SSE transports are deprecated
The HTTP+SSE client and server transports (and their supporting validator/exception types) are deprecated in favour of Streamable HTTP — `HttpClientStreamableHttpTransport` on the client, and `HttpServletStreamableServerTransportProvider` on the server. They still work; plan a move to Streamable HTTP.
---
## New features
These are additive and backward-compatible.
### URL elicitation (SEP-1036)
Servers can request out-of-band URL input from users (for example payment or API-key flows) during tool execution. Adds `ElicitUrlRequest`, `urlElicitation()` / `elicitationCompleteConsumer(s)()` builder methods on `McpClient`, `sendElicitationComplete()` on `McpAsyncServer`/`McpSyncServer`, the `ElicitationCompleteNotification` record, and the `URL_ELICITATION_REQUIRED` error code. See the [`ElicitRequest` interface change](#elicitrequest-is-now-an-interface) for the related breaking change.
### Client-side elicitation defaults (SEP-1034)
A new opt-in `McpClient` builder option `applyElicitationDefaults(boolean)` fills missing keys of an accepted `ElicitResult.content` with the `default` values declared in the request's `requestedSchema` before returning the result to the server. It is a local client config, not a wire capability.
### Icons and metadata (SEP-973)
A new `Icon` record and an optional `icons` field were added to `Implementation`, `Resource`, `ResourceTemplate`, `Prompt`, and `Tool`. `Implementation` also gains optional `description` and `websiteUrl` fields. All fields are optional; existing constructors and builders are unchanged.
### `_meta` on paginated list queries
The client list operations accept an optional `_meta` map alongside the pagination cursor: `listResources(String cursor, Map<String, Object> meta)`, `listResourceTemplates(...)`, `listPrompts(...)`, and `listTools(...)`.

View file

@ -47,20 +47,21 @@ The client provides both synchronous and asynchronous APIs for flexibility in di
// Call a tool
CallToolResult result = client.callTool(
new CallToolRequest("calculator",
Map.of("operation", "add", "a", 2, "b", 3))
CallToolRequest.builder("calculator")
.arguments(Map.of("operation", "add", "a", 2, "b", 3))
.build()
);
// List and read resources
ListResourcesResult resources = client.listResources();
ReadResourceResult resource = client.readResource(
new ReadResourceRequest("resource://uri")
ReadResourceRequest.builder("resource://uri").build()
);
// List and use prompts
ListPromptsResult prompts = client.listPrompts();
GetPromptResult prompt = client.getPrompt(
new GetPromptRequest("greeting", Map.of("name", "Spring"))
GetPromptRequest.builder("greeting").arguments(Map.of("name", "Spring")).build()
);
// Add/remove roots
@ -102,24 +103,22 @@ The client provides both synchronous and asynchronous APIs for flexibility in di
client.initialize()
.flatMap(initResult -> client.listTools())
.flatMap(tools -> {
return client.callTool(new CallToolRequest(
"calculator",
Map.of("operation", "add", "a", 2, "b", 3)
));
return client.callTool(CallToolRequest.builder("calculator")
.arguments(Map.of("operation", "add", "a", 2, "b", 3))
.build());
})
.flatMap(result -> {
return client.listResources()
.flatMap(resources ->
client.readResource(new ReadResourceRequest("resource://uri"))
client.readResource(ReadResourceRequest.builder("resource://uri").build())
);
})
.flatMap(resource -> {
return client.listPrompts()
.flatMap(prompts ->
client.getPrompt(new GetPromptRequest(
"greeting",
Map.of("name", "Spring")
))
client.getPrompt(GetPromptRequest.builder("greeting")
.arguments(Map.of("name", "Spring"))
.build())
);
})
.flatMap(prompt -> {
@ -144,7 +143,7 @@ Creates transport for process-based communication using stdin/stdout:
ServerParameters params = ServerParameters.builder("npx")
.args("-y", "@modelcontextprotocol/server-everything", "dir")
.build();
McpTransport transport = new StdioClientTransport(params);
McpTransport transport = new StdioClientTransport(params, McpJsonDefaults.getMapper());
```
### Streamable HTTP
@ -184,7 +183,7 @@ McpTransport transport = new StdioClientTransport(params);
Creates a framework-agnostic (pure Java API) SSE client transport. Included in the core `mcp` module:
```java
McpTransport transport = new HttpClientSseClientTransport("http://your-mcp-server");
McpTransport transport = HttpClientSseClientTransport.builder("http://your-mcp-server").build();
```
=== "SSE WebClient (external)"
@ -301,6 +300,17 @@ The `ElicitResult` supports three actions:
- `DECLINE` - The user declined to provide the information
- `CANCEL` - The operation was cancelled
You can optionally have the client fill in missing values from the schema's `default` declarations before returning an accepted result to the server:
```java
var client = McpClient.sync(transport)
.applyElicitationDefaults(true) // default is false
.elicitation(formElicitationHandler)
.build();
```
When enabled, any keys absent from an accepted `ElicitResult.content` are populated with the `default` values declared in the request's `requestedSchema`.
#### URL Elicitation Required Handling
When a server requires out-of-band URL elicitation but the client has not negotiated support for it (or the server strictly requires out-of-band handling), the server may return a `URL_ELICITATION_REQUIRED` error during tool execution or prompt retrieval.
@ -339,7 +349,7 @@ mcpClient.initialize();
mcpClient.setLoggingLevel(McpSchema.LoggingLevel.INFO);
// Call the tool that sends logging notifications
CallToolResult result = mcpClient.callTool(new CallToolRequest("logging-test", Map.of()));
CallToolResult result = mcpClient.callTool(CallToolRequest.builder("logging-test").build());
```
Clients can control the minimum logging level they receive through the `mcpClient.setLoggingLevel(level)` request. Messages below the set level will be filtered out.
@ -371,11 +381,13 @@ Tools are server-side functions that clients can discover and execute. The MCP c
// Call a tool with a CallToolRequest
CallToolResult result = client.callTool(
new CallToolRequest("calculator", Map.of(
"operation", "add",
"a", 1,
"b", 2
))
CallToolRequest.builder("calculator")
.arguments(Map.of(
"operation", "add",
"a", 1,
"b", 2
))
.build()
);
```
@ -389,11 +401,13 @@ Tools are server-side functions that clients can discover and execute. The MCP c
.subscribe();
// Call a tool asynchronously
client.callTool(new CallToolRequest("calculator", Map.of(
"operation", "add",
"a", 1,
"b", 2
)))
client.callTool(CallToolRequest.builder("calculator")
.arguments(Map.of(
"operation", "add",
"a", 1,
"b", 2
))
.build())
.subscribe();
```
@ -420,7 +434,7 @@ Resources represent server-side data sources that clients can access using URI t
// Read a resource
ReadResourceResult resource = client.readResource(
new ReadResourceRequest("resource://uri")
ReadResourceRequest.builder("resource://uri").build()
);
```
@ -434,7 +448,7 @@ Resources represent server-side data sources that clients can access using URI t
.subscribe();
// Read a resource asynchronously
client.readResource(new ReadResourceRequest("resource://uri"))
client.readResource(ReadResourceRequest.builder("resource://uri").build())
.subscribe();
```
@ -457,10 +471,10 @@ Register a consumer on the client builder, then subscribe/unsubscribe at any tim
client.initialize();
// Subscribe to a specific resource URI
client.subscribeResource(new McpSchema.SubscribeRequest("custom://resource"));
client.subscribeResource(McpSchema.SubscribeRequest.builder("custom://resource").build());
// ... later, stop receiving updates
client.unsubscribeResource(new McpSchema.UnsubscribeRequest("custom://resource"));
client.unsubscribeResource(McpSchema.UnsubscribeRequest.builder("custom://resource").build());
```
=== "Async API"
@ -473,11 +487,11 @@ Register a consumer on the client builder, then subscribe/unsubscribe at any tim
.build();
client.initialize()
.then(client.subscribeResource(new McpSchema.SubscribeRequest("custom://resource")))
.then(client.subscribeResource(McpSchema.SubscribeRequest.builder("custom://resource").build()))
.subscribe();
// ... later, stop receiving updates
client.unsubscribeResource(new McpSchema.UnsubscribeRequest("custom://resource"))
client.unsubscribeResource(McpSchema.UnsubscribeRequest.builder("custom://resource").build())
.subscribe();
```
@ -493,7 +507,7 @@ The prompt system enables interaction with server-side prompt templates. These t
// Get a prompt with parameters
GetPromptResult prompt = client.getPrompt(
new GetPromptRequest("greeting", Map.of("name", "World"))
GetPromptRequest.builder("greeting").arguments(Map.of("name", "World")).build()
);
```
@ -507,6 +521,6 @@ The prompt system enables interaction with server-side prompt templates. These t
.subscribe();
// Get a prompt asynchronously
client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "World")))
client.getPrompt(GetPromptRequest.builder("greeting").arguments(Map.of("name", "World")).build())
.subscribe();
```

View file

@ -123,7 +123,7 @@ Add the BOM to your project:
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-bom</artifactId>
<version>1.0.0</version>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -135,7 +135,7 @@ Add the BOM to your project:
```groovy
dependencies {
implementation platform("io.modelcontextprotocol.sdk:mcp-bom:1.0.0")
implementation platform("io.modelcontextprotocol.sdk:mcp-bom:2.0.0")
//...
}
```

View file

@ -111,7 +111,7 @@ Create process-based transport using stdin/stdout:
```java
StdioServerTransportProvider transportProvider =
new StdioServerTransportProvider(new ObjectMapper());
new StdioServerTransportProvider(McpJsonDefaults.getMapper());
```
Provides bidirectional JSON-RPC message handling over standard input/output streams with non-blocking message processing, serialization/deserialization, and graceful shutdown support.
@ -237,7 +237,9 @@ Key features:
@Bean
public HttpServletSseServerTransportProvider servletSseServerTransportProvider() {
return new HttpServletSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
return HttpServletSseServerTransportProvider.builder()
.messageEndpoint("/mcp/message")
.build();
}
@Bean
@ -340,10 +342,8 @@ The recommended approach is to use the builder pattern and `CallToolRequest` as
```java
// Sync tool specification using builder
var syncToolSpecification = SyncToolSpecification.builder()
.tool(Tool.builder()
.name("calculator")
.tool(Tool.builder("calculator", schema)
.description("Basic calculator")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Access arguments via request.arguments()
@ -363,10 +363,8 @@ The recommended approach is to use the builder pattern and `CallToolRequest` as
```java
// Async tool specification using builder
var asyncToolSpecification = AsyncToolSpecification.builder()
.tool(Tool.builder()
.name("calculator")
.tool(Tool.builder("calculator", schema)
.description("Basic calculator")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Access arguments via request.arguments()
@ -389,7 +387,7 @@ You can also register tools directly on the server builder using the `toolCall`
```java
var server = McpServer.sync(transportProvider)
.toolCall(
Tool.builder().name("echo").description("Echoes input").inputSchema(schema).build(),
Tool.builder("echo", schema).description("Echoes input").build(),
(exchange, request) -> CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(request.arguments().get("text").toString())))
.build()
@ -397,6 +395,18 @@ var server = McpServer.sync(transportProvider)
.build();
```
#### Tool Input Validation
By default the server validates incoming tool arguments against the tool's `inputSchema` before invoking the handler. When validation fails, the call returns a `CallToolResult` with `isError` set and a textual error, rather than reaching your handler. Validation uses the configured `JsonSchemaValidator` (or the default from `McpJsonDefaults.getSchemaValidator()`), and can be turned off on the server builder:
```java
var server = McpServer.sync(transportProvider)
.validateToolInputs(false) // default is true
.build();
```
The embedded JSON Schema documents themselves (`Tool.inputSchema`, `Tool.outputSchema`, and elicitation `requestedSchema`) are validated against the JSON Schema 2020-12 meta-schema (SEP-1613). Malformed schemas are rejected at build time (`McpServer.build()`) and when calling `addTool()`, throwing an `IllegalArgumentException` that names the offending field. A schema that declares a different dialect via `$schema` is accepted without meta-schema validation.
### Resource Specification
Specification of a resource with its handler function.
@ -407,15 +417,13 @@ Resources provide context to AI models by exposing data such as: File contents,
```java
// Sync resource specification
var syncResourceSpecification = new McpServerFeatures.SyncResourceSpecification(
Resource.builder()
.uri("custom://resource")
.name("name")
Resource.builder("custom://resource", "name")
.description("description")
.mimeType("text/plain")
.build(),
(exchange, request) -> {
// Resource read implementation
return new ReadResourceResult(contents);
return ReadResourceResult.builder(contents).build();
}
);
```
@ -425,15 +433,13 @@ Resources provide context to AI models by exposing data such as: File contents,
```java
// Async resource specification
var asyncResourceSpecification = new McpServerFeatures.AsyncResourceSpecification(
Resource.builder()
.uri("custom://resource")
.name("name")
Resource.builder("custom://resource", "name")
.description("description")
.mimeType("text/plain")
.build(),
(exchange, request) -> {
// Resource read implementation
return Mono.just(new ReadResourceResult(contents));
return Mono.just(ReadResourceResult.builder(contents).build());
}
);
```
@ -481,15 +487,13 @@ Resource templates allow servers to expose parameterized resources using URI tem
```java
// Resource template specification
var resourceTemplateSpec = new McpServerFeatures.SyncResourceTemplateSpecification(
ResourceTemplate.builder()
.uriTemplate("file://{path}")
.name("File Resource")
ResourceTemplate.builder("file://{path}", "File Resource")
.description("Access files by path")
.mimeType("application/octet-stream")
.build(),
(exchange, request) -> {
// Read the file at the requested URI
return new ReadResourceResult(contents);
return ReadResourceResult.builder(contents).build();
}
);
```
@ -504,12 +508,18 @@ The Prompt Specification is a structured template for AI model interactions that
```java
// Sync prompt specification
var syncPromptSpecification = new McpServerFeatures.SyncPromptSpecification(
new Prompt("greeting", "description", List.of(
new PromptArgument("name", "description", true)
)),
Prompt.builder("greeting")
.description("description")
.arguments(List.of(
PromptArgument.builder("name")
.description("description")
.required(true)
.build()
))
.build(),
(exchange, request) -> {
// Prompt implementation
return new GetPromptResult(description, messages);
return GetPromptResult.builder(messages).description(description).build();
}
);
```
@ -519,12 +529,18 @@ The Prompt Specification is a structured template for AI model interactions that
```java
// Async prompt specification
var asyncPromptSpecification = new McpServerFeatures.AsyncPromptSpecification(
new Prompt("greeting", "description", List.of(
new PromptArgument("name", "description", true)
)),
Prompt.builder("greeting")
.description("description")
.arguments(List.of(
PromptArgument.builder("name")
.description("description")
.required(true)
.build()
))
.build(),
(exchange, request) -> {
// Prompt implementation
return Mono.just(new GetPromptResult(description, messages));
return Mono.just(GetPromptResult.builder(messages).description(description).build());
}
);
```
@ -592,10 +608,8 @@ Once connected to a compatible client, the server can request language model gen
// Define a tool that uses sampling
var calculatorTool = SyncToolSpecification.builder()
.tool(Tool.builder()
.name("ai-calculator")
.tool(Tool.builder("ai-calculator", schema)
.description("Performs calculations using AI")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Check if client supports sampling
@ -606,9 +620,10 @@ Once connected to a compatible client, the server can request language model gen
}
// Create a sampling request
CreateMessageRequest samplingRequest = CreateMessageRequest.builder()
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))))
CreateMessageRequest samplingRequest = CreateMessageRequest.builder(
List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))),
100)
.modelPreferences(McpSchema.ModelPreferences.builder()
.hints(List.of(
McpSchema.ModelHint.of("claude-3-sonnet"),
@ -618,7 +633,6 @@ Once connected to a compatible client, the server can request language model gen
.speedPriority(0.5)
.build())
.systemPrompt("You are a helpful calculator assistant. Provide only the numerical answer.")
.maxTokens(100)
.build();
// Request sampling from the client
@ -646,10 +660,8 @@ Once connected to a compatible client, the server can request language model gen
// Define a tool that uses sampling
var calculatorTool = AsyncToolSpecification.builder()
.tool(Tool.builder()
.name("ai-calculator")
.tool(Tool.builder("ai-calculator", schema)
.description("Performs calculations using AI")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Check if client supports sampling
@ -660,9 +672,10 @@ Once connected to a compatible client, the server can request language model gen
}
// Create a sampling request
CreateMessageRequest samplingRequest = CreateMessageRequest.builder()
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))))
CreateMessageRequest samplingRequest = CreateMessageRequest.builder(
List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))),
100)
.modelPreferences(McpSchema.ModelPreferences.builder()
.hints(List.of(
McpSchema.ModelHint.of("claude-3-sonnet"),
@ -672,7 +685,6 @@ Once connected to a compatible client, the server can request language model gen
.speedPriority(0.5)
.build())
.systemPrompt("You are a helpful calculator assistant. Provide only the numerical answer.")
.maxTokens(100)
.build();
// Request sampling from the client
@ -701,10 +713,8 @@ Servers can request user input from connected clients that support elicitation:
```java
var tool = SyncToolSpecification.builder()
.tool(Tool.builder()
.name("confirm-action")
.tool(Tool.builder("confirm-action", schema)
.description("Confirms an action with the user")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Check if client supports elicitation
@ -741,10 +751,8 @@ To request out-of-band URL elicitation, such as a user authorizing an OAuth flow
```java
var urlTool = SyncToolSpecification.builder()
.tool(Tool.builder()
.name("oauth-auth")
.tool(Tool.builder("oauth-auth", schema)
.description("Authenticates via OAuth")
.inputSchema(schema)
.build())
.callHandler((exchange, request) -> {
// Request URL elicitation from client
@ -779,23 +787,17 @@ Log notifications can only be sent from within an existing client session, such
The server can send log messages using the `McpAsyncServerExchange`/`McpSyncServerExchange` object in the tool/resource/prompt handler function:
```java
var tool = new McpServerFeatures.AsyncToolSpecification(
Tool.builder().name("logging-test").description("Test logging notifications").inputSchema(emptyJsonSchema).build(),
null,
(exchange, request) -> {
exchange.loggingNotification( // Use the exchange to send log messages
McpSchema.LoggingMessageNotification.builder()
.level(McpSchema.LoggingLevel.DEBUG)
.logger("test-logger")
.data("Debug message")
.build())
.block();
return Mono.just(CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Logging test completed")))
.build());
});
var tool = AsyncToolSpecification.builder()
.tool(Tool.builder("logging-test", emptyJsonSchema).description("Test logging notifications").build())
.callHandler((exchange, request) ->
exchange.loggingNotification( // Use the exchange to send log messages
McpSchema.LoggingMessageNotification.builder(McpSchema.LoggingLevel.DEBUG, "Debug message")
.logger("test-logger")
.build())
.then(Mono.just(CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Logging test completed")))
.build())))
.build();
var mcpServer = McpServer.async(mcpServerTransportProvider)
.serverInfo("test-server", "1.0.0")