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

View file

@ -80,7 +80,7 @@ public class ConformanceJdkClientMcpClient {
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build();
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))
.build();
}
@ -97,7 +97,7 @@ public class ConformanceJdkClientMcpClient {
var capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
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))
.capabilities(capabilities)
.elicitation(request -> {
@ -120,7 +120,7 @@ public class ConformanceJdkClientMcpClient {
}
// 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();
}
@ -174,7 +174,7 @@ public class ConformanceJdkClientMcpClient {
arguments.put("b", 3);
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");
if (result != null && result.content() != null) {
@ -219,7 +219,9 @@ public class ConformanceJdkClientMcpClient {
var arguments = new java.util.HashMap<String, Object>();
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");
if (result != null && result.content() != null) {
@ -264,8 +266,8 @@ public class ConformanceJdkClientMcpClient {
// reconnection
var arguments = new java.util.HashMap<String, Object>();
McpSchema.CallToolResult result = client
.callTool(new McpSchema.CallToolRequest("test_reconnection", arguments));
McpSchema.CallToolResult result = client.callTool(
McpSchema.CallToolRequest.builder("test_reconnection").arguments(arguments).build());
System.out.println("Successfully called test_reconnection tool");
if (result != null && result.content() != null) {

View file

@ -69,7 +69,7 @@ public class DefaultScenario implements Scenario {
this.client = McpClient.sync(transport)
.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))
.build();

View file

@ -60,7 +60,7 @@ public class PreRegistrationScenario implements Scenario {
var client = McpClient.sync(transport)
.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))
.build();

View file

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