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
64 changed files with 4258 additions and 1688 deletions

View file

@ -77,23 +77,36 @@ git checkout -b feature/your-feature-name
## Evolving wire-serialized records ## Evolving wire-serialized records
Records in `McpSchema` are serialized directly to the MCP JSON wire format. Follow these rules whenever you add a field to an existing record to keep the protocol forward- and backward-compatible. Records in `McpSchema` are serialized directly to the MCP JSON wire format. The rules differ depending on whether the field you are adding (or maintaining) is *optional* — Java code may legitimately leave it `null` and the wire may omit it — or *spec-required* by MCP. Follow **Case A** for optional fields and **Case B** for spec-required fields.
### Rules ### Case A — Optional fields
1. **Add new components only at the end** of the record's component list. Never reorder or rename existing components. 1. **Add new components only at the end** of the record's component list. Never reorder or rename existing components.
2. **Annotate every component** with `@JsonProperty("fieldName")` even when the Java name already matches. This survives local renames via refactoring tools. 2. **Annotate every component** with `@JsonProperty("fieldName")` even when the Java name already matches. This survives local renames via refactoring tools.
3. **Use boxed types** (`Boolean`, `Integer`, `Long`, `Double`) so the field can be absent on the wire without a special sentinel. 3. **Use boxed types** (`Boolean`, `Integer`, `Long`, `Double`) so the field can be absent on the wire without a special sentinel.
4. **Default to `null`**, not an empty collection or neutral value, so the `@JsonInclude(NON_NULL)` rule omits the field for clients that don't know about it yet. 4. **Default to `null`**, not an empty collection or neutral value, so the `@JsonInclude(NON_ABSENT)` rule omits the field for clients that don't know about it yet.
5. **Keep existing constructors as source-compatible overloads** that delegate to the new canonical constructor and pass `null` for the new component. Do not remove them in the same release that adds the field. 5. **Keep existing constructors as source-compatible overloads** that delegate to the new canonical constructor and pass `null` for the new component. Do not remove them in the same release that adds the field.
6. **Do not put `@JsonCreator` on the canonical constructor** unless strictly necessary. Jackson auto-detects record canonical constructors; adding `@JsonCreator` pins deserialization to that exact parameter order forever. 6. **Do not put `@JsonCreator` on the canonical constructor** unless strictly necessary. Jackson auto-detects record canonical constructors; adding `@JsonCreator` pins deserialization to that exact parameter order forever. *(For records that also have spec-required fields, the `@JsonCreator` belongs on a separate static `fromJson` factory — see Case B, Rule 2.)*
7. **Do not convert `null` to a default value in the canonical constructor.** Null carries "absent" semantics and must be preserved through the serialization round-trip. 7. **Do not convert `null` to a default value in the canonical constructor.** Null carries "absent" semantics and must be preserved through the serialization round-trip. *(Spec-required fields are the exception — see Case B, Rule 1.)*
8. **Add three tests per new field** (put them in the relevant test class in `mcp-test`): 8. **Add three tests per new field** (put them in the relevant test class in `mcp-test`):
- Deserialize JSON *without* the field → succeeds, field is `null`. - Deserialize JSON *without* the field → succeeds, field is `null`.
- Serialize an instance with the field unset (`null`) → the key is absent from output. - Serialize an instance with the field unset (`null`) → the key is absent from output.
- Deserialize JSON with an extra *unknown* field → succeeds. - Deserialize JSON with an extra *unknown* field → succeeds.
9. **An inner `Builder` subclass can be used.** This improves the developer experience since frequently not all fields are required. 9. **An inner `Builder` subclass can be used.** This improves the developer experience since frequently not all fields are required.
### Case B — Spec-required fields
When the MCP specification marks a field as required, callers must not be able to construct a structurally invalid record, but the wire parser must still tolerate peers that fail to send it. Follow these rules in addition to the relevant Case A rules (annotation, naming, append-only).
1. **Reject `null` in the compact constructor.** Use `Assert.notNull` for required objects or `Assert.hasText` for required `String` identifiers (`name`, `uri`, `uriTemplate`, `version`). This throws `IllegalArgumentException` at construction time instead of producing a record that fails later in serialization or protocol handling. Overrides Case A Rule 7 for this field.
2. **Add a `@JsonCreator` static `fromJson` factory** alongside the canonical constructor. When a required field is absent from the wire, substitute a documented safe default (`""` for strings, `[]` for collections, `{}` for maps, `0` / `0.0` for numerics, `INFO` for `LoggingLevel`, etc.) and log at `WARN` naming the field and the value used. The SDK must not halt the conversation because of a missing field. Place `@JsonCreator` on this `fromJson` factory, never on the canonical constructor (Case A Rule 6 still applies to the canonical constructor itself).
- Exception: `JSONRPCResponse.JSONRPCError` fails fast on missing `code` / `message` because a malformed JSON-RPC error envelope is unrecoverable.
3. **Provide a required-first builder factory** `builder(req1, req2, …)` and remove the corresponding setters from the `Builder`. A no-arg `builder()` factory must not exist on a record that has required fields. If one already exists for source compatibility, mark it `@Deprecated`.
4. **Add tests per required field**:
- Constructing the record with `null` for the field throws `IllegalArgumentException`.
- Deserializing JSON *without* the field succeeds and yields the documented default.
- Deserializing JSON with an extra *unknown* field still succeeds (Case A Rule 8 also applies).
### Example ### Example
Suppose `ToolAnnotations` gains an optional `audience` field: Suppose `ToolAnnotations` gains an optional `audience` field:

View file

@ -77,6 +77,97 @@ The `Tool` record now models `inputSchema` (and `outputSchema`) as arbitrary JSO
- Java code that used `Tool.inputSchema()` as a `JsonSchema` must switch to `Map<String, Object>` (or copy into your own schema wrapper). - 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)`. - `Tool.Builder.inputSchema(JsonSchema)` remains as a **deprecated** helper that maps the old record into a map; prefer `inputSchema(Map)` or `inputSchema(McpJsonMapper, String)`.
### Required MCP spec fields are enforced at construction time
Every wire record in `McpSchema` whose fields are marked required by the MCP spec now asserts non-null (and non-empty for `String` identifiers like `name`, `uri`, `uriTemplate`, `version`) in its compact constructor. Passing `null` throws `IllegalArgumentException` immediately, instead of producing a structurally invalid object that fails later in serialization or protocol handling.
This applies to (non-exhaustive):
- JSON-RPC envelopes: `JSONRPCRequest`, `JSONRPCNotification`, `JSONRPCResponse`, `JSONRPCResponse.JSONRPCError`
- Lifecycle: `InitializeRequest`, `InitializeResult`, `Implementation`
- Resources: `Resource`, `ResourceTemplate`, `ListResourcesResult`, `ListResourceTemplatesResult`, `ReadResourceRequest`, `ReadResourceResult`, `SubscribeRequest`, `UnsubscribeRequest`, `ResourcesUpdatedNotification`, `TextResourceContents`, `BlobResourceContents`
- Prompts: `Prompt`, `PromptArgument`, `PromptMessage`, `ListPromptsResult`, `GetPromptRequest`, `GetPromptResult`
- Tools: `Tool`, `ListToolsResult`, `CallToolRequest`, `CallToolResult`
- Sampling / elicitation: `SamplingMessage`, `CreateMessageRequest`, `CreateMessageResult`, `ElicitRequest`, `ElicitResult`
- Misc: `ProgressNotification`, `SetLevelRequest`, `LoggingMessageNotification`, `CompleteRequest`, `CompleteResult`, `CompleteRequest.CompleteArgument`, content records (`TextContent`, `ImageContent`, `AudioContent`, `EmbeddedResource`), `Root`, `ListRootsResult`, `PromptReference`, `ResourceReference`
**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.
**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.
### `PromptReference` discriminator pinning and equality
`PromptReference` keeps its `(type, name, title)` record components, so positional construction from 1.x still compiles. Two behavioural changes:
- 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:** 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.
### 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:
| Type | Old (deprecated) | New |
|------|-----------------|-----|
| `Resource` | `Resource.builder().uri(u).name(n)…` | `Resource.builder(uri, name)…` |
| `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)…` |
| `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)…` |
| `ElicitRequest` | `… .builder().message(m).requestedSchema(s)…` | `… .builder(message, requestedSchema)…` |
| `LoggingMessageNotification` | `… .builder().level(l).data(d)…` | `… .builder(level, data)…` |
| `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)…` |
| `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()…` |
### 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:
```java
new JSONRPCRequest("2.0", "tools/call", id, params);
new JSONRPCNotification("2.0", "notifications/initialized", null);
new JSONRPCResponse("2.0", id, result, null);
new JSONRPCResponse("2.0", id, null, new JSONRPCError(code, message, null));
```
2.0 adds defaulting constructors and static factories so the `"2.0"` constant and the unused `result`/`error` slot disappear from caller code:
```java
new JSONRPCRequest("tools/call", id); // params optional
new JSONRPCRequest("tools/call", id, params);
new JSONRPCNotification("notifications/initialized"); // params optional
new JSONRPCNotification("notifications/initialized", params);
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.
The 1.x canonical 4-arg constructors continue to compile.
### Optional JSON Schema validation on `tools/call` (server) ### Optional JSON Schema validation on `tools/call` (server)
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. 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.

View file

@ -80,7 +80,7 @@ public class ConformanceJdkClientMcpClient {
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build(); HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build();
return McpClient.sync(transport) return McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")) .clientInfo(McpSchema.Implementation.builder("test-client", "1.0.0").build())
.requestTimeout(Duration.ofSeconds(30)) .requestTimeout(Duration.ofSeconds(30))
.build(); .build();
} }
@ -97,7 +97,7 @@ public class ConformanceJdkClientMcpClient {
var capabilities = McpSchema.ClientCapabilities.builder().elicitation().build(); var capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
return McpClient.sync(transport) return McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")) .clientInfo(McpSchema.Implementation.builder("test-client", "1.0.0").build())
.requestTimeout(Duration.ofSeconds(30)) .requestTimeout(Duration.ofSeconds(30))
.capabilities(capabilities) .capabilities(capabilities)
.elicitation(request -> { .elicitation(request -> {
@ -120,7 +120,7 @@ public class ConformanceJdkClientMcpClient {
} }
// Return accept action with the defaults applied // Return accept action with the defaults applied
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, content, null); return McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).content(content).build();
}) })
.build(); .build();
} }
@ -174,7 +174,7 @@ public class ConformanceJdkClientMcpClient {
arguments.put("b", 3); arguments.put("b", 3);
McpSchema.CallToolResult result = client McpSchema.CallToolResult result = client
.callTool(new McpSchema.CallToolRequest("add_numbers", arguments)); .callTool(McpSchema.CallToolRequest.builder("add_numbers").arguments(arguments).build());
System.out.println("Successfully called add_numbers tool"); System.out.println("Successfully called add_numbers tool");
if (result != null && result.content() != null) { if (result != null && result.content() != null) {
@ -219,7 +219,9 @@ public class ConformanceJdkClientMcpClient {
var arguments = new java.util.HashMap<String, Object>(); var arguments = new java.util.HashMap<String, Object>();
McpSchema.CallToolResult result = client McpSchema.CallToolResult result = client
.callTool(new McpSchema.CallToolRequest("test_client_elicitation_defaults", arguments)); .callTool(McpSchema.CallToolRequest.builder("test_client_elicitation_defaults")
.arguments(arguments)
.build());
System.out.println("Successfully called test_client_elicitation_defaults tool"); System.out.println("Successfully called test_client_elicitation_defaults tool");
if (result != null && result.content() != null) { if (result != null && result.content() != null) {
@ -264,8 +266,8 @@ public class ConformanceJdkClientMcpClient {
// reconnection // reconnection
var arguments = new java.util.HashMap<String, Object>(); var arguments = new java.util.HashMap<String, Object>();
McpSchema.CallToolResult result = client McpSchema.CallToolResult result = client.callTool(
.callTool(new McpSchema.CallToolRequest("test_reconnection", arguments)); McpSchema.CallToolRequest.builder("test_reconnection").arguments(arguments).build());
System.out.println("Successfully called test_reconnection tool"); System.out.println("Successfully called test_reconnection tool");
if (result != null && result.content() != null) { if (result != null && result.content() != null) {

View file

@ -69,7 +69,7 @@ public class DefaultScenario implements Scenario {
this.client = McpClient.sync(transport) this.client = McpClient.sync(transport)
.transportContextProvider(new AuthenticationMcpTransportContextProvider()) .transportContextProvider(new AuthenticationMcpTransportContextProvider())
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")) .clientInfo(McpSchema.Implementation.builder("test-client", "1.0.0").build())
.requestTimeout(Duration.ofSeconds(30)) .requestTimeout(Duration.ofSeconds(30))
.build(); .build();

View file

@ -60,7 +60,7 @@ public class PreRegistrationScenario implements Scenario {
var client = McpClient.sync(transport) var client = McpClient.sync(transport)
.transportContextProvider(new AuthenticationMcpTransportContextProvider()) .transportContextProvider(new AuthenticationMcpTransportContextProvider())
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0")) .clientInfo(McpSchema.Implementation.builder("test-client", "1.0.0").build())
.requestTimeout(Duration.ofSeconds(30)) .requestTimeout(Duration.ofSeconds(30))
.build(); .build();

View file

@ -144,15 +144,14 @@ public class ConformanceServlet {
return List.of( return List.of(
// test_simple_text - Returns simple text content // test_simple_text - Returns simple text content
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_simple_text", EMPTY_JSON_SCHEMA)
.name("test_simple_text")
.description("Returns simple text content for testing") .description("Returns simple text content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_simple_text' called"); logger.info("Tool 'test_simple_text' called");
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent("This is a simple text response for testing."))) .content(
List.of(TextContent.builder("This is a simple text response for testing.").build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -160,15 +159,13 @@ public class ConformanceServlet {
// test_image_content - Returns image content // test_image_content - Returns image content
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_image_content", EMPTY_JSON_SCHEMA)
.name("test_image_content")
.description("Returns image content for testing") .description("Returns image content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_image_content' called"); logger.info("Tool 'test_image_content' called");
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new ImageContent(null, RED_PIXEL_PNG, "image/png"))) .content(List.of(ImageContent.builder(RED_PIXEL_PNG, "image/png").build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -176,15 +173,13 @@ public class ConformanceServlet {
// test_audio_content - Returns audio content // test_audio_content - Returns audio content
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_audio_content", EMPTY_JSON_SCHEMA)
.name("test_audio_content")
.description("Returns audio content for testing") .description("Returns audio content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_audio_content' called"); logger.info("Tool 'test_audio_content' called");
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new AudioContent(null, MINIMAL_WAV, "audio/wav"))) .content(List.of(AudioContent.builder(MINIMAL_WAV, "audio/wav").build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -192,36 +187,35 @@ public class ConformanceServlet {
// test_embedded_resource - Returns embedded resource content // test_embedded_resource - Returns embedded resource content
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_embedded_resource", EMPTY_JSON_SCHEMA)
.name("test_embedded_resource")
.description("Returns embedded resource content for testing") .description("Returns embedded resource content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_embedded_resource' called"); logger.info("Tool 'test_embedded_resource' called");
TextResourceContents resourceContents = new TextResourceContents("test://embedded-resource", TextResourceContents resourceContents = TextResourceContents
"text/plain", "This is an embedded resource content."); .builder("test://embedded-resource", "This is an embedded resource content.")
EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents); .mimeType("text/plain")
.build();
EmbeddedResource embeddedResource = EmbeddedResource.builder(resourceContents).build();
return CallToolResult.builder().content(List.of(embeddedResource)).isError(false).build(); return CallToolResult.builder().content(List.of(embeddedResource)).isError(false).build();
}) })
.build(), .build(),
// test_multiple_content_types - Returns multiple content types // test_multiple_content_types - Returns multiple content types
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_multiple_content_types", EMPTY_JSON_SCHEMA)
.name("test_multiple_content_types")
.description("Returns multiple content types for testing") .description("Returns multiple content types for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_multiple_content_types' called"); logger.info("Tool 'test_multiple_content_types' called");
TextResourceContents resourceContents = new TextResourceContents( TextResourceContents resourceContents = TextResourceContents
"test://mixed-content-resource", "application/json", .builder("test://mixed-content-resource", "{\"test\":\"data\",\"value\":123}")
"{\"test\":\"data\",\"value\":123}"); .mimeType("application/json")
EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents); .build();
EmbeddedResource embeddedResource = EmbeddedResource.builder(resourceContents).build();
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent("Multiple content types test:"), .content(List.of(TextContent.builder("Multiple content types test:").build(),
new ImageContent(null, RED_PIXEL_PNG, "image/png"), embeddedResource)) ImageContent.builder(RED_PIXEL_PNG, "image/png").build(), embeddedResource))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -229,28 +223,22 @@ public class ConformanceServlet {
// test_tool_with_logging - Tool that sends log messages during execution // test_tool_with_logging - Tool that sends log messages during execution
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_tool_with_logging", EMPTY_JSON_SCHEMA)
.name("test_tool_with_logging")
.description("Tool that sends log messages during execution") .description("Tool that sends log messages during execution")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_tool_with_logging' called"); logger.info("Tool 'test_tool_with_logging' called");
// Send log notifications // Send log notifications
exchange.loggingNotification(LoggingMessageNotification.builder() exchange.loggingNotification(
.level(LoggingLevel.INFO) LoggingMessageNotification.builder(LoggingLevel.INFO, "Tool execution started")
.data("Tool execution started") .build());
.build()); exchange.loggingNotification(
exchange.loggingNotification(LoggingMessageNotification.builder() LoggingMessageNotification.builder(LoggingLevel.INFO, "Tool processing data").build());
.level(LoggingLevel.INFO) exchange.loggingNotification(
.data("Tool processing data") LoggingMessageNotification.builder(LoggingLevel.INFO, "Tool execution completed")
.build()); .build());
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Tool execution completed")
.build());
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent("Tool execution completed with logging"))) .content(List.of(TextContent.builder("Tool execution completed with logging").build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -258,15 +246,14 @@ public class ConformanceServlet {
// test_error_handling - Tool that always returns an error // test_error_handling - Tool that always returns an error
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_error_handling", EMPTY_JSON_SCHEMA)
.name("test_error_handling")
.description("Tool that returns an error for testing error handling") .description("Tool that returns an error for testing error handling")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_error_handling' called"); logger.info("Tool 'test_error_handling' called");
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent("This tool intentionally returns an error for testing"))) .content(List.of(TextContent.builder("This tool intentionally returns an error for testing")
.build()))
.isError(true) .isError(true)
.build(); .build();
}) })
@ -274,33 +261,34 @@ public class ConformanceServlet {
// test_tool_with_progress - Tool that reports progress // test_tool_with_progress - Tool that reports progress
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_tool_with_progress", EMPTY_JSON_SCHEMA)
.name("test_tool_with_progress")
.description("Tool that reports progress notifications") .description("Tool that reports progress notifications")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_tool_with_progress' called"); logger.info("Tool 'test_tool_with_progress' called");
Object progressToken = request.meta().get("progressToken"); Object progressToken = request.meta().get("progressToken");
if (progressToken != null) { if (progressToken != null) {
// Send progress notifications sequentially // Send progress notifications sequentially
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 100.0, null)); exchange.progressNotification(
ProgressNotification.builder(progressToken, 0.0).total(100.0).build());
// try { // try {
// Thread.sleep(50); // Thread.sleep(50);
// } // }
// catch (InterruptedException e) { // catch (InterruptedException e) {
// Thread.currentThread().interrupt(); // Thread.currentThread().interrupt();
// } // }
exchange.progressNotification(new ProgressNotification(progressToken, 50.0, 100.0, null)); exchange.progressNotification(
ProgressNotification.builder(progressToken, 50.0).total(100.0).build());
// try { // try {
// Thread.sleep(50); // Thread.sleep(50);
// } // }
// catch (InterruptedException e) { // catch (InterruptedException e) {
// Thread.currentThread().interrupt(); // Thread.currentThread().interrupt();
// } // }
exchange.progressNotification(new ProgressNotification(progressToken, 100.0, 100.0, null)); exchange.progressNotification(
ProgressNotification.builder(progressToken, 100.0).total(100.0).build());
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent("Tool execution completed with progress"))) .content(List.of(TextContent.builder("Tool execution completed with progress").build()))
.isError(false) .isError(false)
.build(); .build();
} }
@ -313,7 +301,8 @@ public class ConformanceServlet {
// Thread.currentThread().interrupt(); // Thread.currentThread().interrupt();
// } // }
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent("Tool execution completed without progress"))) .content(List
.of(TextContent.builder("Tool execution completed without progress").build()))
.isError(false) .isError(false)
.build(); .build();
} }
@ -322,28 +311,28 @@ public class ConformanceServlet {
// test_sampling - Tool that requests LLM sampling from client // test_sampling - Tool that requests LLM sampling from client
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool
.name("test_sampling") .builder("test_sampling", Map.of("type", "object", "properties",
.description("Tool that requests LLM sampling from client")
.inputSchema(Map.of("type", "object", "properties",
Map.of("prompt", Map.of("prompt",
Map.of("type", "string", "description", "The prompt to send to the LLM")), Map.of("type", "string", "description", "The prompt to send to the LLM")),
"required", List.of("prompt"))) "required", List.of("prompt")))
.description("Tool that requests LLM sampling from client")
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_sampling' called"); logger.info("Tool 'test_sampling' called");
String prompt = (String) request.arguments().get("prompt"); String prompt = (String) request.arguments().get("prompt");
// Request sampling from client // Request sampling from client
CreateMessageRequest samplingRequest = CreateMessageRequest.builder() CreateMessageRequest samplingRequest = CreateMessageRequest
.messages(List.of(new SamplingMessage(Role.USER, new TextContent(prompt)))) .builder(List
.maxTokens(100) .of(SamplingMessage.builder(Role.USER, TextContent.builder(prompt).build()).build()),
100)
.build(); .build();
CreateMessageResult response = exchange.createMessage(samplingRequest); CreateMessageResult response = exchange.createMessage(samplingRequest);
String responseText = "LLM response: " + ((TextContent) response.content()).text(); String responseText = "LLM response: " + ((TextContent) response.content()).text();
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent(responseText))) .content(List.of(TextContent.builder(responseText).build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -351,13 +340,12 @@ public class ConformanceServlet {
// test_elicitation - Tool that requests user input from client // test_elicitation - Tool that requests user input from client
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool
.name("test_elicitation") .builder("test_elicitation", Map.of("type", "object", "properties",
.description("Tool that requests user input from client")
.inputSchema(Map.of("type", "object", "properties",
Map.of("message", Map.of("message",
Map.of("type", "string", "description", "The message to show the user")), Map.of("type", "string", "description", "The message to show the user")),
"required", List.of("message"))) "required", List.of("message")))
.description("Tool that requests user input from client")
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation' called"); logger.info("Tool 'test_elicitation' called");
@ -369,13 +357,13 @@ public class ConformanceServlet {
Map.of("type", "string", "description", "User's email address")), Map.of("type", "string", "description", "User's email address")),
"required", List.of("username", "email")); "required", List.of("username", "email"));
ElicitRequest elicitRequest = new ElicitRequest(message, requestedSchema); ElicitRequest elicitRequest = ElicitRequest.builder(message, requestedSchema).build();
ElicitResult response = exchange.createElicitation(elicitRequest); ElicitResult response = exchange.createElicitation(elicitRequest);
String responseText = "User response: action=" + response.action() + ", content=" String responseText = "User response: action=" + response.action() + ", content="
+ response.content(); + response.content();
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent(responseText))) .content(List.of(TextContent.builder(responseText).build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -384,10 +372,8 @@ public class ConformanceServlet {
// test_elicitation_sep1034_defaults - Tool with default values for all // test_elicitation_sep1034_defaults - Tool with default values for all
// primitive types // primitive types
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_elicitation_sep1034_defaults", EMPTY_JSON_SCHEMA)
.name("test_elicitation_sep1034_defaults")
.description("Tool that requests elicitation with default values for all primitive types") .description("Tool that requests elicitation with default values for all primitive types")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation_sep1034_defaults' called"); logger.info("Tool 'test_elicitation_sep1034_defaults' called");
@ -402,14 +388,15 @@ public class ConformanceServlet {
"verified", Map.of("type", "boolean", "default", true)), "verified", Map.of("type", "boolean", "default", true)),
"required", List.of("name", "age", "score", "status", "verified")); "required", List.of("name", "age", "score", "status", "verified"));
ElicitRequest elicitRequest = new ElicitRequest("Please provide your information with defaults", ElicitRequest elicitRequest = ElicitRequest
requestedSchema); .builder("Please provide your information with defaults", requestedSchema)
.build();
ElicitResult response = exchange.createElicitation(elicitRequest); ElicitResult response = exchange.createElicitation(elicitRequest);
String responseText = "Elicitation completed: action=" + response.action() + ", content=" String responseText = "Elicitation completed: action=" + response.action() + ", content="
+ response.content(); + response.content();
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent(responseText))) .content(List.of(TextContent.builder(responseText).build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -417,10 +404,8 @@ public class ConformanceServlet {
// test_elicitation_sep1330_enums - Tool with enum schema improvements // test_elicitation_sep1330_enums - Tool with enum schema improvements
McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder() .tool(Tool.builder("test_elicitation_sep1330_enums", EMPTY_JSON_SCHEMA)
.name("test_elicitation_sep1330_enums")
.description("Tool that requests elicitation with enum schema improvements") .description("Tool that requests elicitation with enum schema improvements")
.inputSchema(EMPTY_JSON_SCHEMA)
.build()) .build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation_sep1330_enums' called"); logger.info("Tool 'test_elicitation_sep1330_enums' called");
@ -455,13 +440,14 @@ public class ConformanceServlet {
"required", List.of("untitledSingle", "titledSingle", "legacyEnum", "untitledMulti", "required", List.of("untitledSingle", "titledSingle", "legacyEnum", "untitledMulti",
"titledMulti")); "titledMulti"));
ElicitRequest elicitRequest = new ElicitRequest("Select your preferences", requestedSchema); ElicitRequest elicitRequest = ElicitRequest.builder("Select your preferences", requestedSchema)
.build();
ElicitResult response = exchange.createElicitation(elicitRequest); ElicitResult response = exchange.createElicitation(elicitRequest);
String responseText = "Elicitation completed: action=" + response.action() + ", content=" String responseText = "Elicitation completed: action=" + response.action() + ", content="
+ response.content(); + response.content();
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new TextContent(responseText))) .content(List.of(TextContent.builder(responseText).build()))
.isError(false) .isError(false)
.build(); .build();
}) })
@ -471,113 +457,142 @@ public class ConformanceServlet {
private static List<McpServerFeatures.SyncPromptSpecification> createPromptSpecs() { private static List<McpServerFeatures.SyncPromptSpecification> createPromptSpecs() {
return List.of( return List.of(
// test_simple_prompt - Simple prompt without arguments // test_simple_prompt - Simple prompt without arguments
new McpServerFeatures.SyncPromptSpecification( new McpServerFeatures.SyncPromptSpecification(Prompt.builder("test_simple_prompt")
new Prompt("test_simple_prompt", null, "A simple prompt for testing", List.of()), .description("A simple prompt for testing")
(exchange, request) -> { .arguments(List.of())
logger.info("Prompt 'test_simple_prompt' requested"); .build(), (exchange, request) -> {
return new GetPromptResult(null, List.of(new PromptMessage(Role.USER, logger.info("Prompt 'test_simple_prompt' requested");
new TextContent("This is a simple prompt for testing.")))); return GetPromptResult.builder(List.of(PromptMessage
}), .builder(Role.USER, TextContent.builder("This is a simple prompt for testing.").build())
.build())).build();
}),
// test_prompt_with_arguments - Prompt with arguments // test_prompt_with_arguments - Prompt with arguments
new McpServerFeatures.SyncPromptSpecification( new McpServerFeatures.SyncPromptSpecification(Prompt.builder("test_prompt_with_arguments")
new Prompt("test_prompt_with_arguments", null, "A prompt with arguments for testing", .description("A prompt with arguments for testing")
List.of(new PromptArgument("arg1", "First test argument", true), .arguments(List.of(
new PromptArgument("arg2", "Second test argument", true))), PromptArgument.builder("arg1").description("First test argument").required(true).build(),
(exchange, request) -> { PromptArgument.builder("arg2").description("Second test argument").required(true).build()))
logger.info("Prompt 'test_prompt_with_arguments' requested"); .build(), (exchange, request) -> {
String arg1 = (String) request.arguments().get("arg1"); logger.info("Prompt 'test_prompt_with_arguments' requested");
String arg2 = (String) request.arguments().get("arg2"); String arg1 = (String) request.arguments().get("arg1");
String text = String.format("Prompt with arguments: arg1='%s', arg2='%s'", arg1, arg2); String arg2 = (String) request.arguments().get("arg2");
return new GetPromptResult(null, String text = String.format("Prompt with arguments: arg1='%s', arg2='%s'", arg1, arg2);
List.of(new PromptMessage(Role.USER, new TextContent(text)))); return GetPromptResult
}), .builder(List
.of(PromptMessage.builder(Role.USER, TextContent.builder(text).build()).build()))
.build();
}),
// test_prompt_with_embedded_resource - Prompt with embedded resource // test_prompt_with_embedded_resource - Prompt with embedded resource
new McpServerFeatures.SyncPromptSpecification( new McpServerFeatures.SyncPromptSpecification(Prompt.builder("test_prompt_with_embedded_resource")
new Prompt("test_prompt_with_embedded_resource", null, .description("A prompt with embedded resource for testing")
"A prompt with embedded resource for testing", .arguments(List.of(PromptArgument.builder("resourceUri")
List.of(new PromptArgument("resourceUri", "URI of the resource to embed", true))), .description("URI of the resource to embed")
(exchange, request) -> { .required(true)
logger.info("Prompt 'test_prompt_with_embedded_resource' requested"); .build()))
String resourceUri = (String) request.arguments().get("resourceUri"); .build(), (exchange, request) -> {
TextResourceContents resourceContents = new TextResourceContents(resourceUri, "text/plain", logger.info("Prompt 'test_prompt_with_embedded_resource' requested");
"Embedded resource content for testing."); String resourceUri = (String) request.arguments().get("resourceUri");
EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents); TextResourceContents resourceContents = TextResourceContents
return new GetPromptResult(null, .builder(resourceUri, "Embedded resource content for testing.")
List.of(new PromptMessage(Role.USER, embeddedResource), new PromptMessage(Role.USER, .mimeType("text/plain")
new TextContent("Please process the embedded resource above.")))); .build();
}), EmbeddedResource embeddedResource = EmbeddedResource.builder(resourceContents).build();
return GetPromptResult
.builder(List.of(PromptMessage.builder(Role.USER, embeddedResource).build(),
PromptMessage
.builder(Role.USER,
TextContent.builder("Please process the embedded resource above.")
.build())
.build()))
.build();
}),
// test_prompt_with_image - Prompt with image content // test_prompt_with_image - Prompt with image content
new McpServerFeatures.SyncPromptSpecification(new Prompt("test_prompt_with_image", null, new McpServerFeatures.SyncPromptSpecification(Prompt.builder("test_prompt_with_image")
"A prompt with image content for testing", List.of()), (exchange, request) -> { .description("A prompt with image content for testing")
logger.info("Prompt 'test_prompt_with_image' requested"); .arguments(List.of())
return new GetPromptResult(null, List.of( .build(), (exchange, request) -> {
new PromptMessage(Role.USER, new ImageContent(null, RED_PIXEL_PNG, "image/png")), logger.info("Prompt 'test_prompt_with_image' requested");
new PromptMessage(Role.USER, new TextContent("Please analyze the image above.")))); return GetPromptResult.builder(List.of(
})); PromptMessage
.builder(Role.USER, ImageContent.builder(RED_PIXEL_PNG, "image/png").build())
.build(),
PromptMessage
.builder(Role.USER, TextContent.builder("Please analyze the image above.").build())
.build()))
.build();
}));
} }
private static List<McpServerFeatures.SyncResourceSpecification> createResourceSpecs() { private static List<McpServerFeatures.SyncResourceSpecification> createResourceSpecs() {
return List.of( return List.of(
// test://static-text - Static text resource // test://static-text - Static text resource
new McpServerFeatures.SyncResourceSpecification(Resource.builder() new McpServerFeatures.SyncResourceSpecification(
.uri("test://static-text") Resource.builder("test://static-text", "Static Text Resource")
.name("Static Text Resource") .description("A static text resource for testing")
.description("A static text resource for testing") .mimeType("text/plain")
.mimeType("text/plain") .build(),
.build(), (exchange, request) -> { (exchange, request) -> {
logger.info("Resource 'test://static-text' requested"); logger.info("Resource 'test://static-text' requested");
return new ReadResourceResult(List.of(new TextResourceContents("test://static-text", return ReadResourceResult.builder(List.of(TextResourceContents
"text/plain", "This is the content of the static text resource."))); .builder("test://static-text", "This is the content of the static text resource.")
}), .mimeType("text/plain")
.build())).build();
}),
// test://static-binary - Static binary resource (image) // test://static-binary - Static binary resource (image)
new McpServerFeatures.SyncResourceSpecification(Resource.builder() new McpServerFeatures.SyncResourceSpecification(
.uri("test://static-binary") Resource.builder("test://static-binary", "Static Binary Resource")
.name("Static Binary Resource") .description("A static binary resource for testing")
.description("A static binary resource for testing") .mimeType("image/png")
.mimeType("image/png") .build(),
.build(), (exchange, request) -> { (exchange, request) -> {
logger.info("Resource 'test://static-binary' requested"); logger.info("Resource 'test://static-binary' requested");
return new ReadResourceResult( return ReadResourceResult
List.of(new BlobResourceContents("test://static-binary", "image/png", RED_PIXEL_PNG))); .builder(List.of(BlobResourceContents.builder("test://static-binary", RED_PIXEL_PNG)
}), .mimeType("image/png")
.build()))
.build();
}),
// test://watched-resource - Resource that can be subscribed to // test://watched-resource - Resource that can be subscribed to
new McpServerFeatures.SyncResourceSpecification(Resource.builder() new McpServerFeatures.SyncResourceSpecification(
.uri("test://watched-resource") Resource.builder("test://watched-resource", "Watched Resource")
.name("Watched Resource") .description("A resource that can be subscribed to for updates")
.description("A resource that can be subscribed to for updates") .mimeType("text/plain")
.mimeType("text/plain") .build(),
.build(), (exchange, request) -> { (exchange, request) -> {
logger.info("Resource 'test://watched-resource' requested"); logger.info("Resource 'test://watched-resource' requested");
return new ReadResourceResult(List.of(new TextResourceContents("test://watched-resource", return ReadResourceResult.builder(List.of(TextResourceContents
"text/plain", "This is a watched resource content."))); .builder("test://watched-resource", "This is a watched resource content.")
})); .mimeType("text/plain")
.build())).build();
}));
} }
private static List<McpServerFeatures.SyncResourceTemplateSpecification> createResourceTemplateSpecs() { private static List<McpServerFeatures.SyncResourceTemplateSpecification> createResourceTemplateSpecs() {
return List.of( return List.of(
// test://template/{id}/data - Resource template with parameter // test://template/{id}/data - Resource template with parameter
// substitution // substitution
new McpServerFeatures.SyncResourceTemplateSpecification(ResourceTemplate.builder() new McpServerFeatures.SyncResourceTemplateSpecification(
.uriTemplate("test://template/{id}/data") ResourceTemplate.builder("test://template/{id}/data", "Template Resource")
.name("Template Resource") .description("A resource template for testing parameter substitution")
.description("A resource template for testing parameter substitution") .mimeType("application/json")
.mimeType("application/json") .build(),
.build(), (exchange, request) -> { (exchange, request) -> {
logger.info("Resource template 'test://template/{{id}}/data' requested for URI: {}", logger.info("Resource template 'test://template/{{id}}/data' requested for URI: {}",
request.uri()); request.uri());
// Extract id from URI // Extract id from URI
String uri = request.uri(); String uri = request.uri();
String id = uri.replaceAll("test://template/(.+)/data", "$1"); String id = uri.replaceAll("test://template/(.+)/data", "$1");
String jsonContent = String String jsonContent = String
.format("{\"id\":\"%s\",\"templateTest\":true,\"data\":\"Data for ID: %s\"}", id, id); .format("{\"id\":\"%s\",\"templateTest\":true,\"data\":\"Data for ID: %s\"}", id, id);
return new ReadResourceResult( return ReadResourceResult.builder(List.of(TextResourceContents.builder(uri, jsonContent)
List.of(new TextResourceContents(uri, "application/json", jsonContent))); .mimeType("application/json")
})); .build())).build();
}));
} }
private static List<McpServerFeatures.SyncCompletionSpecification> createCompletionSpecs() { private static List<McpServerFeatures.SyncCompletionSpecification> createCompletionSpecs() {

View file

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

View file

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

View file

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

View file

@ -11,17 +11,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.modelcontextprotocol.util.Assert; 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 Christian Tzolov
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public class ServerParameters { public class ServerParameters {
// Environment variables to inherit by default // Environment variables to inherit by default
@ -32,13 +30,10 @@ public class ServerParameters {
"SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE") "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"); : Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");
@JsonProperty("command")
private String command; private String command;
@JsonProperty("args")
private List<String> args = new ArrayList<>(); private List<String> args = new ArrayList<>();
@JsonProperty("env")
private Map<String, String> env; private Map<String, String> env;
private ServerParameters(String command, List<String> args, 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()); .build());
} }
return requestHandler.handle(transportContext, request.params()) 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 -> { .onErrorResume(t -> {
McpSchema.JSONRPCResponse.JSONRPCError error; McpSchema.JSONRPCResponse.JSONRPCError error;
if (t instanceof McpError mcpError && mcpError.getJsonRpcError() != null) { if (t instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
@ -45,9 +45,9 @@ class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler {
} }
else { else {
error = new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, 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"; String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
logger.warn(content); logger.warn(content);
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(content))) .content(List.of(McpSchema.TextContent.builder(content).build()))
.isError(true) .isError(true)
.build(); .build();
} }
@ -425,7 +425,7 @@ public class McpAsyncServer {
if (!validation.valid()) { if (!validation.valid()) {
logger.warn("Tool call result validation failed: {}", validation.errorMessage()); logger.warn("Tool call result validation failed: {}", validation.errorMessage());
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.errorMessage()))) .content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build()))
.isError(true) .isError(true)
.build(); .build();
} }
@ -438,7 +438,7 @@ public class McpAsyncServer {
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput()))) .content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()))
.isError(result.isError()) .isError(result.isError())
.structuredContent(result.structuredContent()) .structuredContent(result.structuredContent())
.build(); .build();
@ -529,7 +529,7 @@ public class McpAsyncServer {
return (exchange, params) -> { return (exchange, params) -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList(); 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() .stream()
.map(McpServerFeatures.AsyncResourceSpecification::resource) .map(McpServerFeatures.AsyncResourceSpecification::resource)
.toList(); .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() .stream()
.map(McpServerFeatures.AsyncResourceTemplateSpecification::resourceTemplate) .map(McpServerFeatures.AsyncResourceTemplateSpecification::resourceTemplate)
.toList(); .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) .map(McpServerFeatures.AsyncPromptSpecification::prompt)
.toList(); .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) return this.listRoots(McpSchema.FIRST_PAGE)
.expand(result -> (result.nextCursor() != null) ? .expand(result -> (result.nextCursor() != null) ?
this.listRoots(result.nextCursor()) : Mono.empty()) this.listRoots(result.nextCursor()) : Mono.empty())
.reduce(new McpSchema.ListRootsResult(new ArrayList<>(), null), .reduce(McpSchema.ListRootsResult.builder(new ArrayList<>()).build(),
(allRootsResult, result) -> { (allRootsResult, result) -> {
allRootsResult.roots().addAll(result.roots()); allRootsResult.roots().addAll(result.roots());
return allRootsResult; return allRootsResult;
}) })
.map(result -> new McpSchema.ListRootsResult(Collections.unmodifiableList(result.roots()), .map(result -> McpSchema.ListRootsResult.builder(Collections.unmodifiableList(result.roots()))
result.nextCursor())); .nextCursor(result.nextCursor()).build());
// @formatter:on // @formatter:on
} }

View file

@ -66,9 +66,9 @@ import reactor.core.publisher.Mono;
* Example of creating a basic synchronous server: <pre>{@code * Example of creating a basic synchronous server: <pre>{@code
* McpServer.sync(transportProvider) * McpServer.sync(transportProvider)
* .serverInfo("my-server", "1.0.0") * .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() * (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) * .isError(false)
* .build()) * .build())
* .build(); * .build();
@ -77,10 +77,10 @@ import reactor.core.publisher.Mono;
* Example of creating a basic asynchronous server: <pre>{@code * Example of creating a basic asynchronous server: <pre>{@code
* McpServer.async(transportProvider) * McpServer.async(transportProvider)
* .serverInfo("my-server", "1.0.0") * .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())) * (exchange, request) -> Mono.fromSupplier(() -> calculate(request.arguments()))
* .map(result -> CallToolResult.builder() * .map(result -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + result))) * .content(List.of(McpSchema.TextContent.builder("Result: " + result).build()))
* .isError(false) * .isError(false)
* .build())) * .build()))
* .build(); * .build();
@ -97,7 +97,7 @@ import reactor.core.publisher.Mono;
* .tool(calculatorTool) * .tool(calculatorTool)
* .callTool((exchange, args) -> Mono.fromSupplier(() -> calculate(args.arguments())) * .callTool((exchange, args) -> Mono.fromSupplier(() -> calculate(args.arguments()))
* .map(result -> CallToolResult.builder() * .map(result -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + result))) * .content(List.of(McpSchema.TextContent.builder("Result: " + result).build()))
* .isError(false) * .isError(false)
* .build())) * .build()))
*. .build(), *. .build(),
@ -105,7 +105,7 @@ import reactor.core.publisher.Mono;
* .tool((weatherTool) * .tool((weatherTool)
* .callTool((exchange, args) -> Mono.fromSupplier(() -> getWeather(args.arguments())) * .callTool((exchange, args) -> Mono.fromSupplier(() -> getWeather(args.arguments()))
* .map(result -> CallToolResult.builder() * .map(result -> CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Weather: " + result))) * .content(List.of(McpSchema.TextContent.builder("Weather: " + result).build()))
* .isError(false) * .isError(false)
* .build())) * .build()))
* .build() * .build()
@ -145,7 +145,8 @@ import reactor.core.publisher.Mono;
*/ */
public interface McpServer { 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. * 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) { public AsyncSpecification<S> serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty"); Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version 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; return this;
} }
@ -992,7 +993,7 @@ public interface McpServer {
public SyncSpecification<S> serverInfo(String name, String version) { public SyncSpecification<S> serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty"); Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version 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; return this;
} }
@ -1531,7 +1532,7 @@ public interface McpServer {
public StatelessAsyncSpecification serverInfo(String name, String version) { public StatelessAsyncSpecification serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty"); Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version 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; return this;
} }
@ -2028,7 +2029,7 @@ public interface McpServer {
public StatelessSyncSpecification serverInfo(String name, String version) { public StatelessSyncSpecification serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty"); Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version 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; return this;
} }

View file

@ -77,10 +77,12 @@ public class McpServerFeatures {
// logging // logging
// by // by
// default // default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null, !Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources) !Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null, ? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null); !Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : List.of(); this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of(); this.resources = (resources != null) ? resources : Map.of();
@ -192,10 +194,12 @@ public class McpServerFeatures {
// logging // logging
// by // by
// default // default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null, !Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources) !Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null, ? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null); !Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : new ArrayList<>(); this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>(); this.resources = (resources != null) ? resources : new HashMap<>();
@ -487,20 +491,19 @@ public class McpServerFeatures {
* *
* <pre>{@code * <pre>{@code
* McpServerFeatures.SyncToolSpecification.builder() * McpServerFeatures.SyncToolSpecification.builder()
* .tool(Tool.builder() * .tool(Tool.builder("calculator",
* .name("calculator") * Map.of("type", "object", "properties",
* Map.of("expression", Map.of("type", "string")),
* "required", List.of("expression")))
* .title("Performs mathematical calculations") * .title("Performs mathematical calculations")
* .inputSchema(new JsonSchemaObject() * .build())
* .required("expression")
* .property("expression", JsonSchemaType.STRING))
* .build()
* .toolHandler((exchange, req) -> { * .toolHandler((exchange, req) -> {
* String expr = (String) req.arguments().get("expression"); * String expr = (String) req.arguments().get("expression");
* return CallToolResult.builder() * return CallToolResult.builder()
* .content(List.of(new McpSchema.TextContent("Result: " + evaluate(expr)))) * .content(List.of(McpSchema.TextContent.builder("Result: " + evaluate(expr)).build()))
* .isError(false) * .isError(false)
* .build(); * .build();
* })) * })
* .build(); * .build();
* }</pre> * }</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"; String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
logger.warn(content); logger.warn(content);
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(content))) .content(List.of(McpSchema.TextContent.builder(content).build()))
.isError(true) .isError(true)
.build(); .build();
} }
@ -295,7 +295,7 @@ public class McpStatelessAsyncServer {
if (!validation.valid()) { if (!validation.valid()) {
logger.warn("Tool call result validation failed: {}", validation.errorMessage()); logger.warn("Tool call result validation failed: {}", validation.errorMessage());
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.errorMessage()))) .content(List.of(McpSchema.TextContent.builder(validation.errorMessage()).build()))
.isError(true) .isError(true)
.build(); .build();
} }
@ -308,7 +308,7 @@ public class McpStatelessAsyncServer {
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return CallToolResult.builder() return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput()))) .content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()))
.isError(result.isError()) .isError(result.isError())
.structuredContent(result.structuredContent()) .structuredContent(result.structuredContent())
.build(); .build();
@ -393,7 +393,7 @@ public class McpStatelessAsyncServer {
List<Tool> tools = this.tools.stream() List<Tool> tools = this.tools.stream()
.map(McpStatelessServerFeatures.AsyncToolSpecification::tool) .map(McpStatelessServerFeatures.AsyncToolSpecification::tool)
.toList(); .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() .stream()
.map(McpStatelessServerFeatures.AsyncResourceSpecification::resource) .map(McpStatelessServerFeatures.AsyncResourceSpecification::resource)
.toList(); .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() .stream()
.map(AsyncResourceTemplateSpecification::resourceTemplate) .map(AsyncResourceTemplateSpecification::resourceTemplate)
.toList(); .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) .map(McpStatelessServerFeatures.AsyncPromptSpecification::prompt)
.toList(); .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 : new McpSchema.ServerCapabilities(null, // completions
null, // experimental null, // experimental
null, // currently statless server doesn't support set logging 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) !Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null, ? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null); !Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : List.of(); this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of(); this.resources = (resources != null) ? resources : Map.of();
@ -172,10 +174,12 @@ public class McpStatelessServerFeatures {
// logging // logging
// by // by
// default // default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null, !Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
: null,
!Utils.isEmpty(resources) !Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null, ? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null); !Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
: null);
this.tools = (tools != null) ? tools : new ArrayList<>(); this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>(); 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.setHeader(HttpHeaders.MCP_SESSION_ID, init.session().getId());
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(HttpServletResponse.SC_OK);
String jsonResponse = jsonMapper.writeValueAsString(new McpSchema.JSONRPCResponse( String jsonResponse = jsonMapper
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null)); .writeValueAsString(McpSchema.JSONRPCResponse.result(jsonrpcRequest.id(), initResult));
PrintWriter writer = response.getWriter(); PrintWriter writer = response.getWriter();
writer.write(jsonResponse); writer.write(jsonResponse);

View file

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

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