completionCompleteRequestHandler() {
- return (exchange, params) -> {
- McpSchema.CompleteRequest request = parseCompletionParams(params);
-
- if (request.ref() == null) {
- return Mono.error(new McpError("ref must not be null"));
- }
-
- if (request.ref().type() == null) {
- return Mono.error(new McpError("type must not be null"));
- }
-
- String type = request.ref().type();
-
- String argumentName = request.argument().name();
-
- // check if the referenced resource exists
- if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
- McpServerFeatures.AsyncPromptSpecification promptSpec = this.prompts.get(promptReference.name());
- if (promptSpec == null) {
- return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
- }
- if (!promptSpec.prompt()
- .arguments()
- .stream()
- .filter(arg -> arg.name().equals(argumentName))
- .findFirst()
- .isPresent()) {
-
- return Mono.error(new McpError("Argument not found: " + argumentName));
- }
- }
-
- if (type.equals("ref/resource")
- && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
- McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
- .get(resourceReference.uri());
- if (resourceSpec == null) {
- return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
- }
- if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
- .getVariableNames()
- .contains(argumentName)) {
- return Mono.error(new McpError("Argument not found: " + argumentName));
- }
+ McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
- }
-
- McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
-
- if (specification == null) {
- return Mono.error(new McpError("AsyncCompletionSpecification not found: " + request.ref()));
- }
-
- return specification.completionHandler().apply(exchange, request);
- };
- }
-
- /**
- * Parses the raw JSON-RPC request parameters into a
- * {@link McpSchema.CompleteRequest} object.
- *
- * This method manually extracts the `ref` and `argument` fields from the input
- * map, determines the correct reference type (either prompt or resource), and
- * constructs a fully-typed {@code CompleteRequest} instance.
- * @param object the raw request parameters, expected to be a Map containing "ref"
- * and "argument" entries.
- * @return a {@link McpSchema.CompleteRequest} representing the structured
- * completion request.
- * @throws IllegalArgumentException if the "ref" type is not recognized.
- */
- @SuppressWarnings("unchecked")
- private McpSchema.CompleteRequest parseCompletionParams(Object object) {
- Map params = (Map) object;
- Map refMap = (Map) params.get("ref");
- Map argMap = (Map) params.get("argument");
-
- String refType = (String) refMap.get("type");
-
- McpSchema.CompleteReference ref = switch (refType) {
- case "ref/prompt" -> new McpSchema.PromptReference(refType, (String) refMap.get("name"));
- case "ref/resource" -> new McpSchema.ResourceReference(refType, (String) refMap.get("uri"));
- default -> throw new IllegalArgumentException("Invalid ref type: " + refType);
- };
-
- String argName = (String) argMap.get("name");
- String argValue = (String) argMap.get("value");
- McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument(
- argName, argValue);
-
- return new McpSchema.CompleteRequest(ref, argument);
- }
+ if (specification == null) {
+ return Mono.error(new McpError("AsyncCompletionSpecification not found: " + request.ref()));
+ }
- // ---------------------------------------
- // Sampling
- // ---------------------------------------
+ return specification.completionHandler().apply(exchange, request);
+ };
+ }
- @Override
- void setProtocolVersions(List protocolVersions) {
- this.protocolVersions = protocolVersions;
- }
+ /**
+ * Parses the raw JSON-RPC request parameters into a {@link McpSchema.CompleteRequest}
+ * object.
+ *
+ * This method manually extracts the `ref` and `argument` fields from the input map,
+ * determines the correct reference type (either prompt or resource), and constructs a
+ * fully-typed {@code CompleteRequest} instance.
+ * @param object the raw request parameters, expected to be a Map containing "ref" and
+ * "argument" entries.
+ * @return a {@link McpSchema.CompleteRequest} representing the structured completion
+ * request.
+ * @throws IllegalArgumentException if the "ref" type is not recognized.
+ */
+ @SuppressWarnings("unchecked")
+ private McpSchema.CompleteRequest parseCompletionParams(Object object) {
+ Map params = (Map) object;
+ Map refMap = (Map) params.get("ref");
+ Map argMap = (Map) params.get("argument");
+
+ String refType = (String) refMap.get("type");
+
+ McpSchema.CompleteReference ref = switch (refType) {
+ case "ref/prompt" -> new McpSchema.PromptReference(refType, (String) refMap.get("name"));
+ case "ref/resource" -> new McpSchema.ResourceReference(refType, (String) refMap.get("uri"));
+ default -> throw new IllegalArgumentException("Invalid ref type: " + refType);
+ };
+
+ String argName = (String) argMap.get("name");
+ String argValue = (String) argMap.get("value");
+ McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument(argName,
+ argValue);
+
+ return new McpSchema.CompleteRequest(ref, argument);
+ }
+ /**
+ * This method is package-private and used for test only. Should not be called by user
+ * code.
+ * @param protocolVersions the Client supported protocol versions.
+ */
+ void setProtocolVersions(List protocolVersions) {
+ this.protocolVersions = protocolVersions;
}
}
From b2d3e0098e484e172719237b0933fa395cdfdf4b Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Mon, 12 May 2025 15:04:05 +0200
Subject: [PATCH 027/303] Next development version
Signed-off-by: Christian Tzolov
---
mcp-bom/pom.xml | 2 +-
mcp-spring/mcp-spring-webflux/pom.xml | 6 +++---
mcp-spring/mcp-spring-webmvc/pom.xml | 6 +++---
mcp-test/pom.xml | 4 ++--
mcp/pom.xml | 2 +-
pom.xml | 2 +-
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml
index 4f24f719f..7214dacda 100644
--- a/mcp-bom/pom.xml
+++ b/mcp-bom/pom.xml
@@ -7,7 +7,7 @@
io.modelcontextprotocol.sdk
mcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
mcp-bom
diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml
index 86f46bf95..a8b92bd09 100644
--- a/mcp-spring/mcp-spring-webflux/pom.xml
+++ b/mcp-spring/mcp-spring-webflux/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdk
mcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
../../pom.xml
mcp-spring-webflux
@@ -25,13 +25,13 @@
io.modelcontextprotocol.sdk
mcp
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
io.modelcontextprotocol.sdk
mcp-test
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
test
diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml
index 82fbbf3e6..48d1c3465 100644
--- a/mcp-spring/mcp-spring-webmvc/pom.xml
+++ b/mcp-spring/mcp-spring-webmvc/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdk
mcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
../../pom.xml
mcp-spring-webmvc
@@ -25,13 +25,13 @@
io.modelcontextprotocol.sdk
mcp
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
io.modelcontextprotocol.sdk
mcp-test
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
test
diff --git a/mcp-test/pom.xml b/mcp-test/pom.xml
index f1484ae77..a6e5bdb08 100644
--- a/mcp-test/pom.xml
+++ b/mcp-test/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdk
mcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
mcp-test
jar
@@ -24,7 +24,7 @@
io.modelcontextprotocol.sdk
mcp
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
diff --git a/mcp/pom.xml b/mcp/pom.xml
index 17693ab32..773432827 100644
--- a/mcp/pom.xml
+++ b/mcp/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdk
mcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
mcp
jar
diff --git a/pom.xml b/pom.xml
index 638457406..c2327ee8d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdk
mcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
pom
https://github.com/modelcontextprotocol/java-sdk
From f34662555a0ab68d74ac118f1b0220441b2c81b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Wed, 14 May 2025 15:38:02 +0200
Subject: [PATCH 028/303] Fix stdio tests - proper server-everything argument
(#237)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Dariusz Jędrzejczyk
---
.../modelcontextprotocol/client/StdioMcpAsyncClientTests.java | 4 ++--
.../modelcontextprotocol/client/StdioMcpSyncClientTests.java | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java
index c39080138..8c0069d6d 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java
@@ -25,12 +25,12 @@ protected McpClientTransport createMcpTransport() {
ServerParameters stdioParams;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
stdioParams = ServerParameters.builder("cmd.exe")
- .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "dir")
+ .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "stdio")
.build();
}
else {
stdioParams = ServerParameters.builder("npx")
- .args("-y", "@modelcontextprotocol/server-everything", "dir")
+ .args("-y", "@modelcontextprotocol/server-everything", "stdio")
.build();
}
return new StdioClientTransport(stdioParams);
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
index 8e75c4a3d..706aa9b2e 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
@@ -33,12 +33,12 @@ protected McpClientTransport createMcpTransport() {
ServerParameters stdioParams;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
stdioParams = ServerParameters.builder("cmd.exe")
- .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "dir")
+ .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "stdio")
.build();
}
else {
stdioParams = ServerParameters.builder("npx")
- .args("-y", "@modelcontextprotocol/server-everything", "dir")
+ .args("-y", "@modelcontextprotocol/server-everything", "stdio")
.build();
}
return new StdioClientTransport(stdioParams);
From 2e13f9f9df8610e0d05cc76b1416fe195e249303 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Wed, 14 May 2025 22:46:54 +0200
Subject: [PATCH 029/303] Fix flaky WebFluxSse integration test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Dariusz Jędrzejczyk
---
.../WebFluxSseIntegrationTests.java | 46 ++++++++++---------
1 file changed, 24 insertions(+), 22 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
index 2ba047461..03fbc9962 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
@@ -8,6 +8,8 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
@@ -651,9 +653,11 @@ void testInitialize(String clientType) {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
- void testLoggingNotification(String clientType) {
+ void testLoggingNotification(String clientType) throws InterruptedException {
+ int expectedNotificationsCount = 3;
+ CountDownLatch latch = new CountDownLatch(expectedNotificationsCount);
// Create a list to store received logging notifications
- List receivedNotifications = new ArrayList<>();
+ List receivedNotifications = new CopyOnWriteArrayList<>();
var clientBuilder = clientBuilders.get(clientType);
@@ -709,6 +713,7 @@ void testLoggingNotification(String clientType) {
// Create client with logging notification handler
var mcpClient = clientBuilder.loggingConsumer(notification -> {
receivedNotifications.add(notification);
+ latch.countDown();
}).build()) {
// Initialize client
@@ -724,31 +729,28 @@ void testLoggingNotification(String clientType) {
assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed");
- // Wait for notifications to be processed
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(latch.await(5, TimeUnit.SECONDS)).as("Should receive notifications in reasonable time").isTrue();
- // Should have received 3 notifications (1 NOTICE and 2 ERROR)
- assertThat(receivedNotifications).hasSize(3);
+ // Should have received 3 notifications (1 NOTICE and 2 ERROR)
+ assertThat(receivedNotifications).hasSize(expectedNotificationsCount);
- Map notificationMap = receivedNotifications.stream()
- .collect(Collectors.toMap(n -> n.data(), n -> n));
+ Map notificationMap = receivedNotifications.stream()
+ .collect(Collectors.toMap(n -> n.data(), n -> n));
- // First notification should be NOTICE level
- assertThat(notificationMap.get("Notice message").level()).isEqualTo(McpSchema.LoggingLevel.NOTICE);
- assertThat(notificationMap.get("Notice message").logger()).isEqualTo("test-logger");
- assertThat(notificationMap.get("Notice message").data()).isEqualTo("Notice message");
+ // First notification should be NOTICE level
+ assertThat(notificationMap.get("Notice message").level()).isEqualTo(McpSchema.LoggingLevel.NOTICE);
+ assertThat(notificationMap.get("Notice message").logger()).isEqualTo("test-logger");
+ assertThat(notificationMap.get("Notice message").data()).isEqualTo("Notice message");
- // Second notification should be ERROR level
- assertThat(notificationMap.get("Error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
- assertThat(notificationMap.get("Error message").logger()).isEqualTo("test-logger");
- assertThat(notificationMap.get("Error message").data()).isEqualTo("Error message");
+ // Second notification should be ERROR level
+ assertThat(notificationMap.get("Error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
+ assertThat(notificationMap.get("Error message").logger()).isEqualTo("test-logger");
+ assertThat(notificationMap.get("Error message").data()).isEqualTo("Error message");
- // Third notification should be ERROR level
- assertThat(notificationMap.get("Another error message").level())
- .isEqualTo(McpSchema.LoggingLevel.ERROR);
- assertThat(notificationMap.get("Another error message").logger()).isEqualTo("test-logger");
- assertThat(notificationMap.get("Another error message").data()).isEqualTo("Another error message");
- });
+ // Third notification should be ERROR level
+ assertThat(notificationMap.get("Another error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
+ assertThat(notificationMap.get("Another error message").logger()).isEqualTo("test-logger");
+ assertThat(notificationMap.get("Another error message").data()).isEqualTo("Another error message");
}
mcpServer.close();
}
From 1adfa8a047852c8f9e0188b4e63fe2020e0c66c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Wed, 14 May 2025 14:05:39 +0200
Subject: [PATCH 030/303] Add Contributing Guidelines and Code of Conduct
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Dariusz Jędrzejczyk
---
CODE_OF_CONDUCT.md | 119 +++++++++++++++++++++++++++++++++++++++++++++
CONTRIBUTING.md | 91 ++++++++++++++++++++++++++++++++++
README.md | 7 +--
SECURITY.md | 21 ++++++++
4 files changed, 233 insertions(+), 5 deletions(-)
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 CONTRIBUTING.md
create mode 100644 SECURITY.md
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..6009a645f
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,119 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our community a
+harassment-free experience for everyone, regardless of age, body size, visible or
+invisible disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming, diverse,
+inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our community
+include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes, and
+ learning from the experience
+- Focusing on what is best not just for us as individuals, but for the overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or advances of any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email address, without
+ their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a professional
+ setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in response to
+any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject comments,
+commits, code, wiki edits, issues, and other contributions that are not aligned to this
+Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when an
+individual is officially representing the community in public spaces. Examples of
+representing our community include using an official e-mail address, posting via an
+official social media account, or acting as an appointed representative at an online or
+offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to
+the community leaders responsible for enforcement at mcp-coc@anthropic.com. All
+complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the reporter
+of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining the
+consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing clarity
+around the nature of the violation and an explanation of why the behavior was
+inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No interaction with
+the people involved, including unsolicited interaction with those enforcing the Code of
+Conduct, for a specified period of time. This includes avoiding interactions in community
+spaces as well as external channels like social media. Violating these terms may lead to
+a temporary or permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including sustained
+inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public communication
+with the community for a specified period of time. No public or private interaction with
+the people involved, including unsolicited interaction with those enforcing the Code of
+Conduct, is allowed during this period. Violating these terms may lead to a permanent
+ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community standards,
+including sustained inappropriate behavior, harassment of an individual, or aggression
+toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
+available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..a949dcc09
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,91 @@
+# Contributing to Model Context Protocol Java SDK
+
+Thank you for your interest in contributing to the Model Context Protocol Java SDK!
+This document outlines how to contribute to this project.
+
+## Prerequisites
+
+The following software is required to work on the codebase:
+
+- `Java 17` or above
+- `Docker`
+- `npx`
+
+## Getting Started
+
+1. Fork the repository
+2. Clone your fork:
+
+```bash
+git clone https://github.com/YOUR-USERNAME/java-sdk.git
+cd java-sdk
+```
+
+3. Build from source:
+
+```bash
+./mvnw clean install -DskipTests # skip the tests
+./mvnw test # run tests
+```
+
+## Reporting Issues
+
+Please create an issue in the repository if you discover a bug or would like to
+propose an enhancement. Bug reports should have a reproducer in the form of a code
+sample or a repository attached that the maintainers or contributors can work with to
+address the problem.
+
+## Making Changes
+
+1. Create a new branch:
+
+```bash
+git checkout -b feature/your-feature-name
+```
+
+2. Make your changes
+3. Validate your changes:
+
+```bash
+./mvnw clean test
+```
+
+### Change Proposal Guidelines
+
+#### Principles of MCP
+
+1. **Simple + Minimal**: It is much easier to add things to the codebase than it is to
+ remove them. To maintain simplicity, we keep a high bar for adding new concepts and
+ primitives as each addition requires maintenance and compatibility consideration.
+2. **Concrete**: Code changes need to be based on specific usage and implementation
+ challenges and not on speculative ideas. Most importantly, the SDK is meant to
+ implement the MCP specification.
+
+## Submitting Changes
+
+1. For non-trivial changes, please clarify with the maintainers in an issue whether
+ you can contribute the change and the desired scope of the change.
+2. For trivial changes (for example a couple of lines or documentation changes) there
+ is no need to open an issue first.
+3. Push your changes to your fork.
+4. Submit a pull request to the main repository.
+5. Follow the pull request template.
+6. Wait for review.
+
+## Code of Conduct
+
+This project follows a Code of Conduct. Please review it in
+[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
+
+## Questions
+
+If you have questions, please create a discussion in the repository.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT
+License.
+
+## Security
+
+Please review our [Security Policy](SECURITY.md) for reporting security issues.
\ No newline at end of file
diff --git a/README.md b/README.md
index 9fc17306e..0cd3f84a4 100644
--- a/README.md
+++ b/README.md
@@ -30,11 +30,8 @@ To run the tests you have to pre-install `Docker` and `npx`.
## Contributing
-Contributions are welcome! Please:
-
-1. Fork the repository
-2. Create a feature branch
-3. Submit a Pull Request
+Contributions are welcome!
+Please follow the [Contributing Guidelines](CONTRIBUTING.md).
## Team
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..74e9880fd
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
+
+## Reporting Security Issues
+
+This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model
+Context Protocol project.
+
+The security of our systems and user data is Anthropic’s top priority. We appreciate the
+work of security researchers acting in good faith in identifying and reporting potential
+vulnerabilities.
+
+Our security program is managed on HackerOne and we ask that any validated vulnerability
+in this functionality be reported through their
+[submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
+
+## Vulnerability Disclosure Program
+
+Our Vulnerability Program Guidelines are defined on our
+[HackerOne program page](https://hackerone.com/anthropic-vdp).
\ No newline at end of file
From 07e7b8fd6bac47be4527f97451f8cdd95ed31a38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Wed, 14 May 2025 18:00:06 +0200
Subject: [PATCH 031/303] Add note about force pushes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Dariusz Jędrzejczyk
---
CONTRIBUTING.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a949dcc09..517f32555 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -71,6 +71,9 @@ git checkout -b feature/your-feature-name
4. Submit a pull request to the main repository.
5. Follow the pull request template.
6. Wait for review.
+7. For any follow-up work, please add new commits instead of force-pushing. This will
+ allow the reviewer to focus on incremental changes instead of having to restart the
+ review process.
## Code of Conduct
From 8a5a591d39256ba3947003ec4477e1722363eb35 Mon Sep 17 00:00:00 2001
From: Luca Chang
Date: Tue, 27 May 2025 15:26:44 -0700
Subject: [PATCH 032/303] feat: Add elicitation support to MCP protocol
Implement elicitation capabilities allowing servers to request additional information
from users through clients during interactions. This feature provides a standardized
way for servers to gather necessary information dynamically while clients maintain
control over user interactions and data sharing.
- Add ElicitRequest and ElicitResult classes to McpSchema
- Implement elicitation handlers in client classes
- Add elicitation capabilities to server exchange classes
- Add tests for elicitation functionality with various scenarios
---
.../WebFluxSseIntegrationTests.java | 224 +++++++++++++++++-
.../server/WebMvcSseIntegrationTests.java | 213 +++++++++++++++++
.../client/McpAsyncClient.java | 32 +++
.../client/McpClient.java | 40 +++-
.../client/McpClientFeatures.java | 31 ++-
.../server/McpAsyncServerExchange.java | 28 +++
.../server/McpSyncServerExchange.java | 18 ++
.../modelcontextprotocol/spec/McpSchema.java | 129 ++++++++--
.../client/AbstractMcpAsyncClientTests.java | 22 +-
.../McpAsyncClientResponseHandlerTests.java | 150 ++++++++++++
...rverTransportProviderIntegrationTests.java | 213 +++++++++++++++++
.../spec/McpSchemaTests.java | 34 +++
12 files changed, 1106 insertions(+), 28 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
index 03fbc9962..2f85654e8 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
@@ -4,7 +4,6 @@
package io.modelcontextprotocol;
import java.time.Duration;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -28,11 +27,11 @@
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.*;
-import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.CompletionCapabilities;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
@@ -41,6 +40,7 @@
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.RouterFunctions;
+import reactor.test.StepVerifier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -331,6 +331,226 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt
mcpServer.closeGracefully().block();
}
+ // ---------------------------------------
+ // Elicitation Tests
+ // ---------------------------------------
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCreateElicitationWithoutElicitationCapabilities(String clientType) {
+
+ var clientBuilder = clientBuilders.get(clientType);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ exchange.createElicitation(mock(ElicitRequest.class)).block();
+
+ return Mono.just(mock(CallToolResult.class));
+ });
+
+ var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
+
+ try (
+ // Create client without elicitation capabilities
+ var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) {
+
+ assertThat(client.initialize()).isNotNull();
+
+ try {
+ client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }
+ catch (McpError e) {
+ assertThat(e).isInstanceOf(McpError.class)
+ .hasMessage("Client must be configured with elicitation capabilities");
+ }
+ }
+ server.closeGracefully().block();
+ }
+
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCreateElicitationSuccess(String clientType) {
+
+ var clientBuilder = clientBuilders.get(clientType);
+
+ Function elicitationHandler = request -> {
+ assertThat(request.message()).isNotEmpty();
+ assertThat(request.requestedSchema()).isNotNull();
+
+ return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
+ };
+
+ CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
+ null);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var elicitationRequest = ElicitRequest.builder()
+ .message("Test message")
+ .requestedSchema(
+ Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
+ .build();
+
+ StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
+ assertThat(result.content().get("message")).isEqualTo("Test message");
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .tools(tool)
+ .build();
+
+ try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().elicitation().build())
+ .elicitation(elicitationHandler)
+ .build()) {
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+ }
+ mcpServer.closeGracefully().block();
+ }
+
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCreateElicitationWithRequestTimeoutSuccess(String clientType) {
+
+ // Client
+ var clientBuilder = clientBuilders.get(clientType);
+
+ Function elicitationHandler = request -> {
+ assertThat(request.message()).isNotEmpty();
+ assertThat(request.requestedSchema()).isNotNull();
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
+ };
+
+ var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().elicitation().build())
+ .elicitation(elicitationHandler)
+ .build();
+
+ // Server
+
+ CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
+ null);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var elicitationRequest = ElicitRequest.builder()
+ .message("Test message")
+ .requestedSchema(
+ Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
+ .build();
+
+ StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
+ assertThat(result.content().get("message")).isEqualTo("Test message");
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(3))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+
+ mcpClient.closeGracefully();
+ mcpServer.closeGracefully().block();
+ }
+
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCreateElicitationWithRequestTimeoutFail(String clientType) {
+
+ // Client
+ var clientBuilder = clientBuilders.get(clientType);
+
+ Function elicitationHandler = request -> {
+ assertThat(request.message()).isNotEmpty();
+ assertThat(request.requestedSchema()).isNotNull();
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
+ };
+
+ var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().elicitation().build())
+ .elicitation(elicitationHandler)
+ .build();
+
+ // Server
+
+ CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
+ null);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var elicitationRequest = ElicitRequest.builder()
+ .message("Test message")
+ .requestedSchema(
+ Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
+ .build();
+
+ StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
+ assertThat(result.content().get("message")).isEqualTo("Test message");
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(1))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }).withMessageContaining("within 1000ms");
+
+ mcpClient.closeGracefully();
+ mcpServer.closeGracefully().block();
+ }
+
// ---------------------------------------
// Roots Tests
// ---------------------------------------
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
index b12d68439..3f3f7be62 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
@@ -357,6 +357,219 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException {
mcpServer.close();
}
+ // ---------------------------------------
+ // Elicitation Tests
+ // ---------------------------------------
+ @Test
+ void testCreateElicitationWithoutElicitationCapabilities() {
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ exchange.createElicitation(mock(McpSchema.ElicitRequest.class)).block();
+
+ return Mono.just(mock(CallToolResult.class));
+ });
+
+ var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
+
+ try (
+ // Create client without elicitation capabilities
+ var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) {
+
+ assertThat(client.initialize()).isNotNull();
+
+ try {
+ client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }
+ catch (McpError e) {
+ assertThat(e).isInstanceOf(McpError.class)
+ .hasMessage("Client must be configured with elicitation capabilities");
+ }
+ }
+ server.closeGracefully().block();
+ }
+
+ @Test
+ void testCreateElicitationSuccess() {
+
+ Function elicitationHandler = request -> {
+ assertThat(request.message()).isNotEmpty();
+ assertThat(request.requestedSchema()).isNotNull();
+
+ return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
+ Map.of("message", request.message()));
+ };
+
+ CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
+ null);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var elicitationRequest = McpSchema.ElicitRequest.builder()
+ .message("Test message")
+ .requestedSchema(
+ Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
+ .build();
+
+ StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
+ assertThat(result.content().get("message")).isEqualTo("Test message");
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .tools(tool)
+ .build();
+
+ try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().elicitation().build())
+ .elicitation(elicitationHandler)
+ .build()) {
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+ }
+ mcpServer.closeGracefully().block();
+ }
+
+ @Test
+ void testCreateElicitationWithRequestTimeoutSuccess() {
+
+ // Client
+
+ Function elicitationHandler = request -> {
+ assertThat(request.message()).isNotEmpty();
+ assertThat(request.requestedSchema()).isNotNull();
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
+ Map.of("message", request.message()));
+ };
+
+ var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().elicitation().build())
+ .elicitation(elicitationHandler)
+ .build();
+
+ // Server
+
+ CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
+ null);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var elicitationRequest = McpSchema.ElicitRequest.builder()
+ .message("Test message")
+ .requestedSchema(
+ Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
+ .build();
+
+ StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
+ assertThat(result.content().get("message")).isEqualTo("Test message");
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(3))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+
+ mcpClient.closeGracefully();
+ mcpServer.closeGracefully().block();
+ }
+
+ @Test
+ void testCreateElicitationWithRequestTimeoutFail() {
+
+ // Client
+
+ Function elicitationHandler = request -> {
+ assertThat(request.message()).isNotEmpty();
+ assertThat(request.requestedSchema()).isNotNull();
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
+ Map.of("message", request.message()));
+ };
+
+ var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().elicitation().build())
+ .elicitation(elicitationHandler)
+ .build();
+
+ // Server
+
+ CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
+ null);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var elicitationRequest = McpSchema.ElicitRequest.builder()
+ .message("Test message")
+ .requestedSchema(
+ Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
+ .build();
+
+ StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
+ assertThat(result.content().get("message")).isEqualTo("Test message");
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(1))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }).withMessageContaining("Timeout");
+
+ mcpClient.closeGracefully();
+ mcpServer.closeGracefully().block();
+ }
+
// ---------------------------------------
// Roots Tests
// ---------------------------------------
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index e3a997ba3..a22ef6b51 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -23,6 +23,8 @@
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
+import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
+import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
@@ -141,6 +143,15 @@ public class McpAsyncClient {
*/
private Function> samplingHandler;
+ /**
+ * MCP provides a standardized way for servers to request additional information from
+ * users through the client during interactions. This flow allows clients to maintain
+ * control over user interactions and data sharing while enabling servers to gather
+ * necessary information dynamically. Servers can request structured data from users
+ * with optional JSON schemas to validate responses.
+ */
+ private Function> elicitationHandler;
+
/**
* Client transport implementation.
*/
@@ -189,6 +200,15 @@ public class McpAsyncClient {
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
}
+ // Elicitation Handler
+ if (this.clientCapabilities.elicitation() != null) {
+ if (features.elicitationHandler() == null) {
+ throw new McpError("Elicitation handler must not be null when client capabilities include elicitation");
+ }
+ this.elicitationHandler = features.elicitationHandler();
+ requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
+ }
+
// Notification Handlers
Map notificationHandlers = new HashMap<>();
@@ -500,6 +520,18 @@ private RequestHandler samplingCreateMessageHandler() {
};
}
+ // --------------------------
+ // Elicitation
+ // --------------------------
+ private RequestHandler elicitationCreateHandler() {
+ return params -> {
+ ElicitRequest request = transport.unmarshalFrom(params, new TypeReference<>() {
+ });
+
+ return this.elicitationHandler.apply(request);
+ };
+ }
+
// --------------------------
// Tools
// --------------------------
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
index a1dc11685..280906cff 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
@@ -18,6 +18,8 @@
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
+import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
+import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.util.Assert;
@@ -175,6 +177,8 @@ class SyncSpec {
private Function samplingHandler;
+ private Function elicitationHandler;
+
private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@@ -283,6 +287,21 @@ public SyncSpec sampling(Function sam
return this;
}
+ /**
+ * Sets a custom elicitation handler for processing elicitation message requests.
+ * The elicitation handler can modify or validate messages before they are sent to
+ * the server, enabling custom processing logic.
+ * @param elicitationHandler A function that processes elicitation requests and
+ * returns results. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if elicitationHandler is null
+ */
+ public SyncSpec elicitation(Function elicitationHandler) {
+ Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
+ this.elicitationHandler = elicitationHandler;
+ return this;
+ }
+
/**
* Adds a consumer to be notified when the available tools change. This allows the
* client to react to changes in the server's tool capabilities, such as tools
@@ -364,7 +383,7 @@ public SyncSpec loggingConsumers(List> samplingHandler;
+ private Function> elicitationHandler;
+
private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@@ -522,6 +543,21 @@ public AsyncSpec sampling(Function> elicitationHandler) {
+ Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
+ this.elicitationHandler = elicitationHandler;
+ return this;
+ }
+
/**
* Adds a consumer to be notified when the available tools change. This allows the
* client to react to changes in the server's tool capabilities, such as tools
@@ -606,7 +642,7 @@ public McpAsyncClient build() {
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
- this.loggingConsumers, this.samplingHandler));
+ this.loggingConsumers, this.samplingHandler, this.elicitationHandler));
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
index 284b93f88..23d7c6a60 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java
@@ -60,13 +60,15 @@ class McpClientFeatures {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
+ * @param elicitationHandler the elicitation handler.
*/
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots, List, Mono>> toolsChangeConsumers,
List, Mono>> resourcesChangeConsumers,
List, Mono>> promptsChangeConsumers,
List>> loggingConsumers,
- Function> samplingHandler) {
+ Function> samplingHandler,
+ Function> elicitationHandler) {
/**
* Create an instance and validate the arguments.
@@ -77,6 +79,7 @@ record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
+ * @param elicitationHandler the elicitation handler.
*/
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots,
@@ -84,14 +87,16 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
List, Mono>> resourcesChangeConsumers,
List, Mono>> promptsChangeConsumers,
List>> loggingConsumers,
- Function> samplingHandler) {
+ Function> samplingHandler,
+ Function> elicitationHandler) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
: new McpSchema.ClientCapabilities(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);
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@@ -99,6 +104,7 @@ public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities c
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.samplingHandler = samplingHandler;
+ this.elicitationHandler = elicitationHandler;
}
/**
@@ -138,9 +144,14 @@ public static Async fromSync(Sync syncSpec) {
Function> samplingHandler = r -> Mono
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic());
+
+ Function> elicitationHandler = r -> Mono
+ .fromCallable(() -> syncSpec.elicitationHandler().apply(r))
+ .subscribeOn(Schedulers.boundedElastic());
+
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
- samplingHandler);
+ samplingHandler, elicitationHandler);
}
}
@@ -156,13 +167,15 @@ public static Async fromSync(Sync syncSpec) {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
+ * @param elicitationHandler the elicitation handler.
*/
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots, List>> toolsChangeConsumers,
List>> resourcesChangeConsumers,
List>> promptsChangeConsumers,
List> loggingConsumers,
- Function samplingHandler) {
+ Function samplingHandler,
+ Function elicitationHandler) {
/**
* Create an instance and validate the arguments.
@@ -174,20 +187,23 @@ public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabili
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
+ * @param elicitationHandler the elicitation handler.
*/
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map roots, List>> toolsChangeConsumers,
List>> resourcesChangeConsumers,
List>> promptsChangeConsumers,
List> loggingConsumers,
- Function samplingHandler) {
+ Function samplingHandler,
+ Function elicitationHandler) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
: new McpSchema.ClientCapabilities(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);
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
@@ -195,6 +211,7 @@ public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities cl
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.samplingHandler = samplingHandler;
+ this.elicitationHandler = elicitationHandler;
}
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java
index 889dc66d0..cfb07d26c 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java
@@ -36,6 +36,9 @@ public class McpAsyncServerExchange {
private static final TypeReference LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
};
+ private static final TypeReference ELICITATION_RESULT_TYPE_REF = new TypeReference<>() {
+ };
+
/**
* Create a new asynchronous exchange with the client.
* @param session The server session representing a 1-1 interaction.
@@ -93,6 +96,31 @@ public Mono createMessage(McpSchema.CreateMessage
CREATE_MESSAGE_RESULT_TYPE_REF);
}
+ /**
+ * Creates a new elicitation. MCP provides a standardized way for servers to request
+ * additional information from users through the client during interactions. This flow
+ * allows clients to maintain control over user interactions and data sharing while
+ * enabling servers to gather necessary information dynamically. Servers can request
+ * structured data from users with optional JSON schemas to validate responses.
+ * @param elicitRequest The request to create a new elicitation
+ * @return A Mono that completes when the elicitation has been resolved.
+ * @see McpSchema.ElicitRequest
+ * @see McpSchema.ElicitResult
+ * @see Elicitation
+ * Specification
+ */
+ public Mono createElicitation(McpSchema.ElicitRequest elicitRequest) {
+ if (this.clientCapabilities == null) {
+ return Mono.error(new McpError("Client must be initialized. Call the initialize method first!"));
+ }
+ if (this.clientCapabilities.elicitation() == null) {
+ return Mono.error(new McpError("Client must be configured with elicitation capabilities"));
+ }
+ return this.session.sendRequest(McpSchema.METHOD_ELICITATION_CREATE, elicitRequest,
+ ELICITATION_RESULT_TYPE_REF);
+ }
+
/**
* Retrieves the list of all roots provided by the client.
* @return A Mono that emits the list of roots result.
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java
index 52360e54b..084412b96 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java
@@ -64,6 +64,24 @@ public McpSchema.CreateMessageResult createMessage(McpSchema.CreateMessageReques
return this.exchange.createMessage(createMessageRequest).block();
}
+ /**
+ * Creates a new elicitation. MCP provides a standardized way for servers to request
+ * additional information from users through the client during interactions. This flow
+ * allows clients to maintain control over user interactions and data sharing while
+ * enabling servers to gather necessary information dynamically. Servers can request
+ * structured data from users with optional JSON schemas to validate responses.
+ * @param elicitRequest The request to create a new elicitation
+ * @return A result containing the elicitation response.
+ * @see McpSchema.ElicitRequest
+ * @see McpSchema.ElicitResult
+ * @see Elicitation
+ * Specification
+ */
+ public McpSchema.ElicitResult createElicitation(McpSchema.ElicitRequest elicitRequest) {
+ return this.exchange.createElicitation(elicitRequest).block();
+ }
+
/**
* Retrieves the list of all roots provided by the client.
* @return The list of roots result.
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index 8df8a1584..9dae08266 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -94,6 +94,9 @@ private McpSchema() {
// Sampling Methods
public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage";
+ // Elicitation Methods
+ public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// ---------------------------
@@ -131,8 +134,8 @@ public static final class ErrorCodes {
}
- public sealed interface Request
- permits InitializeRequest, CallToolRequest, CreateMessageRequest, CompleteRequest, GetPromptRequest {
+ public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest, ElicitRequest,
+ CompleteRequest, GetPromptRequest {
}
@@ -221,7 +224,7 @@ public record JSONRPCError(
public record InitializeRequest( // @formatter:off
@JsonProperty("protocolVersion") String protocolVersion,
@JsonProperty("capabilities") ClientCapabilities capabilities,
- @JsonProperty("clientInfo") Implementation clientInfo) implements Request {
+ @JsonProperty("clientInfo") Implementation clientInfo) implements Request {
} // @formatter:on
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@@ -245,6 +248,8 @@ public record InitializeResult( // @formatter:off
* access to.
* @param sampling Provides a standardized way for servers to request LLM sampling
* (“completions” or “generations”) from language models via clients.
+ * @param elicitation Provides a standardized way for servers to request additional
+ * information from users through the client during interactions.
*
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@@ -252,7 +257,8 @@ public record InitializeResult( // @formatter:off
public record ClientCapabilities( // @formatter:off
@JsonProperty("experimental") Map experimental,
@JsonProperty("roots") RootCapabilities roots,
- @JsonProperty("sampling") Sampling sampling) {
+ @JsonProperty("sampling") Sampling sampling,
+ @JsonProperty("elicitation") Elicitation elicitation) {
/**
* Roots define the boundaries of where servers can operate within the filesystem,
@@ -264,7 +270,7 @@ public record ClientCapabilities( // @formatter:off
* has changed since the last time the server checked.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
- @JsonIgnoreProperties(ignoreUnknown = true)
+ @JsonIgnoreProperties(ignoreUnknown = true)
public record RootCapabilities(
@JsonProperty("listChanged") Boolean listChanged) {
}
@@ -279,10 +285,22 @@ public record RootCapabilities(
* image-based interactions and optionally include context
* from MCP servers in their prompts.
*/
- @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record Sampling() {
}
+ /**
+ * Provides a standardized way for servers to request additional
+ * information from users through the client during interactions.
+ * This flow allows clients to maintain control over user
+ * interactions and data sharing while enabling servers to gather
+ * necessary information dynamically. Servers can request structured
+ * data from users with optional JSON schemas to validate responses.
+ */
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ public record Elicitation() {
+ }
+
public static Builder builder() {
return new Builder();
}
@@ -291,6 +309,7 @@ public static class Builder {
private Map