From d5a2968f3f2d0140494b1981bcdc66bbcb060587 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 15 Dec 2025 19:33:00 -0600 Subject: [PATCH 1/4] Enable markdown documentation in javadoc using JDK 23 toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for markdown documentation files (`overview.md`, `doc-files/`) by configuring a JDK 23 toolchain for javadoc generation while keeping JDK 17 as the build target. - Add javadoc-toolchain profile that uses JDK 23 for markdown rendering - Update CI workflows to set up both JDK 17 and JDK 23 - Add markdown `overview.md` as the javadoc landing page - Add documentation for contributors on adding javadoc content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 16 +++++- .github/workflows/maven-central-release.yml | 25 +++++++--- .github/workflows/publish-snapshot.yml | 12 +++-- .../modelcontextprotocol/doc-files/.gitkeep | 0 pom.xml | 25 ++++++++++ src/main/javadoc/README.md | 49 +++++++++++++++++++ src/main/javadoc/overview.md | 32 ++++++++++++ 7 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/.gitkeep create mode 100644 src/main/javadoc/README.md create mode 100644 src/main/javadoc/overview.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c73d9f38..87072332f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: pull_request: {} + workflow_dispatch: {} jobs: build: @@ -11,6 +12,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Set up JDK 23 for Javadoc toolchain + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'temurin' + - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -18,5 +25,12 @@ jobs: distribution: 'temurin' cache: 'maven' - - name: Build + - name: Build and test + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} run: mvn verify + + - name: Validate Javadoc generation + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} + run: mvn -Pjavadoc,javadoc-toolchain javadoc:aggregate diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml index c6c9d3ab6..9946c095c 100644 --- a/.github/workflows/maven-central-release.yml +++ b/.github/workflows/maven-central-release.yml @@ -8,8 +8,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Set up Java + + - name: Set up JDK 23 for Javadoc toolchain + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'temurin' + + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' @@ -25,17 +31,20 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - + - name: Build and Test + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} run: mvn clean verify - name: Publish to Maven Central - run: | - mvn --batch-mode \ - -Prelease \ - -Pjavadoc \ - deploy env: MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} + run: | + mvn --batch-mode \ + -Prelease \ + -Pjavadoc,javadoc-toolchain \ + deploy diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 5d9b4aa39..7af42d4c4 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -12,6 +12,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Set up JDK 23 for Javadoc toolchain + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'temurin' + - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -29,16 +35,14 @@ jobs: with: node-version: '20' - - name: Generate Java docs - run: mvn -Pjavadoc -B javadoc:aggregate - - name: Build with Maven and deploy to Sonatype snapshot repository env: MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} run: | - mvn -Pjavadoc -Prelease --batch-mode --update-snapshots deploy + mvn -Pjavadoc,javadoc-toolchain -Prelease --batch-mode --update-snapshots deploy - name: Capture project version run: echo PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version --quiet -DforceStdout) >> $GITHUB_ENV diff --git a/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/.gitkeep b/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pom.xml b/pom.xml index 67adb4d9a..758ae3047 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,7 @@ 3.1.2 3.5.2 3.11.2 + 23 3.3.0 0.8.10 1.5.0 @@ -266,6 +267,7 @@ + javadoc @@ -287,6 +289,9 @@ ${java.version} true + true + ${project.basedir}/src/main/javadoc + ${project.basedir}/src/main/javadoc/overview.md @@ -308,6 +313,26 @@ + + + javadoc-toolchain + + false + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + ${javadoc.jdk.version} + + + + + + release diff --git a/src/main/javadoc/README.md b/src/main/javadoc/README.md new file mode 100644 index 000000000..b402f078b --- /dev/null +++ b/src/main/javadoc/README.md @@ -0,0 +1,49 @@ +# Javadoc Documentation + +This directory contains documentation that is included in the generated Javadoc. + +## Adding Documentation + +### Overview Page + +Edit `overview.md` in this directory for the main project overview page. + +### Additional Pages + +Place markdown files in `mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/`. + +Example: `mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/getting-started.md` + +## Requirements + +- **JDK 23+** is required to generate javadocs with markdown support (JEP 467) +- Files use CommonMark syntax with GitHub-flavored tables +- Generate docs locally: `mvn -Pjavadoc javadoc:aggregate` + +## Linking to Documentation + +From javadoc comments, link to doc-files: + +```java +/** + * See the Getting Started guide. + */ +``` + +Note: Link to `.html` even though source is `.md` - the javadoc tool converts them. + +From the overview page, use relative links: + +```markdown +See [Getting Started](io/modelcontextprotocol/doc-files/getting-started.html) +``` + +## Markdown Syntax + +The javadoc tool supports CommonMark with these extensions: + +- GitHub-flavored tables +- Fenced code blocks with syntax highlighting +- Links to Java elements: `[text][java.util.List]` + +For full details, see [JEP 467: Markdown Documentation Comments](https://openjdk.org/jeps/467). diff --git a/src/main/javadoc/overview.md b/src/main/javadoc/overview.md new file mode 100644 index 000000000..3aaf52120 --- /dev/null +++ b/src/main/javadoc/overview.md @@ -0,0 +1,32 @@ +# MCP Java SDK + +Java SDK for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), enabling Java applications to interact with AI models and tools through a standardized interface. + +The source code is available at [github.com/modelcontextprotocol/java-sdk](https://github.com/modelcontextprotocol/java-sdk). + +## Modules + +- **mcp** - Convenience module that bundles core dependencies +- **mcp-core** - Core reference implementation +- **mcp-json** - JSON abstraction layer +- **mcp-json-jackson2** - Jackson JSON implementation +- **mcp-spring-webflux** - Spring WebFlux transport +- **mcp-spring-webmvc** - Spring WebMVC transport + +## Getting Started + +Add the MCP SDK to your project: + +```xml + + io.modelcontextprotocol.sdk + mcp + ${mcp.version} + +``` + +## Additional Documentation + +- [MCP documentation](https://modelcontextprotocol.io) +- [MCP specification](https://modelcontextprotocol.io/specification/latest) +- [Spring AI MCP Documentation](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) From 68ca849eb798688b8e96ad784f7ce4791a61d294 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 15 Dec 2025 20:52:29 -0600 Subject: [PATCH 2/4] Add SDK documentation converted from MDX to javadoc markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import and convert documentation from `modelcontextprotocol` repo: - `sdk-overview.md`: Features, architecture, and dependencies - `server.md`: Server implementation and transport providers - `client.md`: Client implementation, transports, and capabilities Conversions applied: - MDX ``/`` elements to markdown subsections - ``/`` callouts to blockquotes - Internal links updated for javadoc doc-files structure - Specification links to `modelcontextprotocol.io/specification/latest` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../modelcontextprotocol/doc-files/client.md | 644 ++++++++++++ .../images/java-mcp-client-architecture.jpg | Bin 0 -> 676694 bytes .../images/java-mcp-server-architecture.jpg | Bin 0 -> 859935 bytes .../doc-files/sdk-overview.md | 174 ++++ .../modelcontextprotocol/doc-files/server.md | 915 ++++++++++++++++++ src/main/javadoc/overview.md | 6 + 6 files changed, 1739 insertions(+) create mode 100644 mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/client.md create mode 100644 mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/images/java-mcp-client-architecture.jpg create mode 100644 mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/images/java-mcp-server-architecture.jpg create mode 100644 mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/sdk-overview.md create mode 100644 mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/server.md diff --git a/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/client.md b/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/client.md new file mode 100644 index 000000000..538cb4b0a --- /dev/null +++ b/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/client.md @@ -0,0 +1,644 @@ +# Java MCP Client + +## Client Features + +The MCP Client is a key component in the Model Context Protocol (MCP) architecture, responsible for establishing and managing connections with MCP servers. It implements the client-side of the protocol, handling: + +- Protocol version negotiation to ensure compatibility with servers +- Capability negotiation to determine available features +- Message transport and JSON-RPC communication +- Tool discovery and execution +- Resource access and management +- Prompt system interactions +- Optional features like roots management and sampling support + +> **Tip:** The core `io.modelcontextprotocol.sdk:mcp` module provides STDIO, Streamable-HTTP and SSE client transport implementations without requiring external web frameworks. +> +> Spring-specific transport implementations are available as an **optional** dependency `io.modelcontextprotocol.sdk:mcp-spring-webflux` for [Spring Framework](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) users. + +> **Tip:** This [quickstart demo](https://modelcontextprotocol.io/quickstart/client), based on Spring AI MCP, will show you how to build an AI client that connects to MCP servers. + +The client provides both synchronous and asynchronous APIs for flexibility in different application contexts. + +### Sync API + +```java +// Create a sync client with custom configuration +McpSyncClient client = McpClient.sync(transport) + .requestTimeout(Duration.ofSeconds(10)) + .capabilities(ClientCapabilities.builder() + .roots(true) // Enable roots capability + .sampling() // Enable sampling capability + .elicitation() // Enable elicitation capability + .build()) + .sampling(request -> CreateMessageResult.builder()...build()) + .elicitation(elicitRequest -> ElicitResult.builder()...build()) + .toolsChangeConsumer((List tools) -> ...) + .resourcesChangeConsumer((List resources) -> ...) + .promptsChangeConsumer((List prompts) -> ...) + .loggingConsumer((LoggingMessageNotification logging) -> ...) + .progressConsumer((ProgressNotification progress) -> ...) + .build(); + +// Initialize connection +client.initialize(); + +// List available tools +ListToolsResult tools = client.listTools(); + +// Call a tool +CallToolResult result = client.callTool( + new CallToolRequest("calculator", + Map.of("operation", "add", "a", 2, "b", 3)) +); + +// List and read resources +ListResourcesResult resources = client.listResources(); +ReadResourceResult resource = client.readResource( + new ReadResourceRequest("resource://uri") +); + +// List and use prompts +ListPromptsResult prompts = client.listPrompts(); +GetPromptResult prompt = client.getPrompt( + new GetPromptRequest("greeting", Map.of("name", "Spring")) +); + +// Add/remove roots +client.addRoot(new Root("file:///path", "description")); +client.removeRoot("file:///path"); + +// Close client +client.closeGracefully(); +``` + +### Async API + +```java +// Create an async client with custom configuration +McpAsyncClient client = McpClient.async(transport) + .requestTimeout(Duration.ofSeconds(10)) + .capabilities(ClientCapabilities.builder() + .roots(true) // Enable roots capability + .sampling() // Enable sampling capability + .elicitation() // Enable elicitation capability + .build()) + .sampling(request -> Mono.just(new CreateMessageResult(response))) + .elicitation(elicitRequest -> Mono.just(ElicitResult.builder()...build())) + .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> logger.info("Tools updated: {}", tools))) + .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> logger.info("Resources updated: {}", resources))) + .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> logger.info("Prompts updated: {}", prompts))) + .loggingConsumer(notification -> Mono.fromRunnable(() -> logger.info("Log: {}", notification.data()))) + .progressConsumer(progress -> Mono.fromRunnable(() -> logger.info("Progress update: {}", progress.data()))) + .build(); + +// Initialize connection and use features +client.initialize() + .flatMap(initResult -> client.listTools()) + .flatMap(tools -> { + return client.callTool(new CallToolRequest( + "calculator", + Map.of("operation", "add", "a", 2, "b", 3) + )); + }) + .flatMap(result -> { + return client.listResources() + .flatMap(resources -> + client.readResource(new ReadResourceRequest("resource://uri")) + ); + }) + .flatMap(resource -> { + return client.listPrompts() + .flatMap(prompts -> + client.getPrompt(new GetPromptRequest( + "greeting", + Map.of("name", "Spring") + )) + ); + }) + .flatMap(prompt -> { + return client.addRoot(new Root("file:///path", "description")) + .then(client.removeRoot("file:///path")); + }) + .doFinally(signalType -> { + client.closeGracefully().subscribe(); + }) + .subscribe(); +``` + +## Client Transport + +The transport layer handles the communication between MCP clients and servers, providing different implementations for various use cases. The client transport manages message serialization, connection establishment, and protocol-specific communication patterns. + +### STDIO Transport + +Creates transport for in-process based communication: + +```java +ServerParameters params = ServerParameters.builder("npx") + .args("-y", "@modelcontextprotocol/server-everything", "dir") + .build(); +McpTransport transport = new StdioClientTransport(params); +``` + +### HttpClient Transport + +#### Streamable-HTTP (HttpClient) + +Framework agnostic (only using JDK APIs) Streamable-HTTP client transport: + +```java +McpTransport transport = HttpClientStreamableHttpTransport + .builder("http://your-mcp-server") + .build(); +``` + +#### SSE (HttpClient) + +Framework agnostic (only using JDK APIs) SSE client transport: + +```java +McpTransport transport = HttpClientSseClientTransport + .builder("http://your-mcp-server") + .build(); +``` + +#### HttpClient: Customizing HTTP requests + +To customize the base HTTP request builder used for every request, provide a custom `HttpRequestBuilder`. +This is available in both Streamable HTTP and SSE transports. +When using a custom `HttpRequest.Builder`, every HTTP request issued by the transport +will use the hardcoded configuration from the builder. +For example, this is useful for providing a never-expiring security token in a header. +To add a `X-Custom-Header` header to every request, use: + +```java +var requestBuilder = HttpRequest + .newBuilder() + .header("X-Custom-Header", "some header value"); + +HttpClientStreamableHttpTransport + .builder("https://mcp.example.com") + .requestBuilder(requestBuilder) + .build(); +``` + +To dynamically modify HTTP request before they are issued, implement either `McpSyncHttpClientRequestCustomizer` or `McpAsyncHttpClientRequestCustomizer`. +Choose the request customizer matching the MCP Client type, sync or async. +Note that thread-locals may not be available in the customizers, context-related information +must be accessed through `McpTransportContext` (see [adding context information](#adding-context-information)). +Example implementations: + +```java +// Sync +class MyRequestCustomizer implements McpSyncHttpClientRequestCustomizer { + @Override + public void customize(HttpRequest.Builder builder, String method, + URI endpoint, String body, McpTransportContext context) { + // ... custom logic ... + var token = obtainAccessToken(context); + builder.header("Authorization", "Bearer " + token); + } +} + +// Async +class MyAsyncRequestCustomizer implements McpAsyncHttpClientRequestCustomizer { + @Override + public Publisher customize(HttpRequest.Builder builder, + String method, URI endpoint, String body, McpTransportContext context) { + // ... custom logic ... + Mono token = obtainAccessToken(context); + return token.map(t -> + builder.copy() + .header("Authorization", "Bearer " + t) + ); + } +} +``` + +The transports, both Streamable HTTP and SSE, can be configured to use a single customizer. + +```java +HttpClientStreamableHttpTransport + .builder("https://mcp.example.com") + .httpRequestCustomizer(new MyRequestCustomizer()) // sync + .asyncHttpRequestCustomizer(new MyAsyncRequestCustomizer()) // OR async + .build(); +``` + +To compose multiple customizers, use `DelegatingMcpSyncHttpClientRequestCustomizer` or `DelegatingMcpAsyncHttpClientRequestCustomizer`. + +### WebClient Transport + +WebClient-based client transport. Requires the `mcp-spring-webflux` dependency. + +#### Streamable-HTTP (WebClient) + +```java +WebClient.Builder webClientBuilder = WebClient.builder() + .baseUrl("http://your-mcp-server"); + +McpTransport transport = WebClientStreamableHttpTransport + .builder(webClientBuilder) + .build(); +``` + +#### SSE (WebClient) + +```java +WebClient.Builder webClientBuilder = WebClient.builder() + .baseUrl("http://your-mcp-server"); + +McpTransport transport = WebFluxSseClientTransport(webClientBuilder) + .builder(webClientBuilder) + .build(); +``` + +#### WebClient: Customizing HTTP requests + +To customize outgoing HTTP requests, provide a custom `WebClient.Builder`. +When using a custom builder, every request sent by the transport will be customized. +For example, this is useful for providing a never-expiring security token in a header. +To add a `X-Custom-Header` header to every request, use: + +```java +var webClientBuilder = WebClient.builder() + .defaultHeader("X-Custom-Header", "some header value"); + +McpTransport transport = WebClientStreamableHttpTransport + .builder(webClientBuilder) + .build(); +``` + +To dynamically modify HTTP request, the builder has a dedicated API, `ExchangeFilterFunction`. +In that function, the new request can be computed at run time, and additional information can +be obtained from the [Reactor context](https://projectreactor.io/docs/core/release/reference/advancedFeatures/context.html). +It is common to store context information in an `McpTransportContext` within the Reactor context, +typically in McpSyncClient, but any context key can be used. For example: + +```java +WebClient.builder() + .filter((request, next) -> { + return Mono.deferContextual(ctx -> { + var transportContext = ctx.get(McpTransportContext.KEY); + var otherInfo = ctx.get("your-context-key"); + Mono token = obtainAccessToken(transportContext, otherInfo); + return token.map(t -> { + var newRequest = ClientRequest.from(request) + .header("Authorization", "Bearer " + t) + .build(); + // Ensure you call next.exchange to execute the HTTP request + return next.exchange(newRequest); + }); + }); + }); +``` + +To learn how to populate context information, see [adding context information](#adding-context-information). + +## Client Capabilities + +The client can be configured with various capabilities: + +```java +var capabilities = ClientCapabilities.builder() + .roots(true) // Enable filesystem roots support with list changes notifications + .sampling() // Enable LLM sampling support + .elicitation() // Enable elicitation capability + .build(); +``` + +### Roots Support + +Roots define the boundaries of where servers can operate within the filesystem: + +```java +// Add a root dynamically +client.addRoot(new Root("file:///path", "description")); + +// Remove a root +client.removeRoot("file:///path"); + +// Notify server of roots changes +client.rootsListChangedNotification(); +``` + +The roots capability allows servers to: + +- Request the list of accessible filesystem roots +- Receive notifications when the roots list changes +- Understand which directories and files they have access to + +### Sampling Support + +Sampling enables servers to request LLM interactions ("completions" or "generations") through the client: + +```java +// Configure sampling handler +Function samplingHandler = request -> { + // Sampling implementation that interfaces with LLM + return new CreateMessageResult(response); +}; + +// Create client with sampling support +var client = McpClient.sync(transport) + .capabilities(ClientCapabilities.builder() + .sampling() + .build()) + .sampling(samplingHandler) + .build(); +``` + +This capability allows: + +- Servers to leverage AI capabilities without requiring API keys +- Clients to maintain control over model access and permissions +- Support for both text and image-based interactions +- Optional inclusion of MCP server context in prompts + +### Elicitation Support + +Elicitation enables servers to request specific information or clarification from the client: + +```java +// Configure elicitation handler +Function elicitationHandler = request -> { + // Elicitation implementation that interfaces with LLM + return ElicitResult.builder()...build(); +}; + +// Create client with elicitation support +var client = McpClient.sync(transport) + .capabilities(ClientCapabilities.builder() + .elicitation() // enable elicitation capability + .build()) + .elicitation(elicitationHandler) // register elicitation handler + .build(); +``` + +### Logging Support + +The client can register a logging consumer to receive log messages from the server and set the minimum logging level to filter messages: + +```java +var mcpClient = McpClient.sync(transport) + .loggingConsumer((LoggingMessageNotification notification) -> { + System.out.println("Received log message: " + notification.data()); + }) + .build(); + +mcpClient.initialize(); + +mcpClient.setLoggingLevel(McpSchema.LoggingLevel.INFO); + +// Call the tool that can sends logging notifications +CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); +``` + +Clients can control the minimum logging level they receive through the `mcpClient.setLoggingLevel(level)` request. Messages below the set level will be filtered out. +Supported logging levels (in order of increasing severity): DEBUG (0), INFO (1), NOTICE (2), WARNING (3), ERROR (4), CRITICAL (5), ALERT (6), EMERGENCY (7) + +### Progress Support + +The client can register a progress consumer to receive progress updates from the server: + +```java +var mcpClient = McpClient.sync(transport) + .progressConsumer((ProgressNotification progress) -> { + System.out.println("Received progress update: " + progress.data()); + }) + .build(); + +mcpClient.initialize(); + +// Call the tool that can sends progress notifications +CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("progress-test", Map.of())); +``` + +### Change Notifications + +The client can register a change consumer to receive change notifications from the server about tools, resources, or prompts updates: + +```java + +var spec = McpClient.sync(transport); + +// Adds a consumer to be notified when the available tools change, such as tools +// being added or removed. +spec.toolsChangeConsumer((List tools) -> { + // Handle tools change +}); + +// Adds a consumer to be notified when the available resources change, such as resources +// being added or removed. +spec.resourcesChangeConsumer((List resources) -> { + // Handle resources change +}); + +// Adds a consumer to be notified when the available prompts change, such as prompts +// being added or removed. +spec.promptsChangeConsumer((List prompts) -> { + // Handle prompts change + +}); +``` + +## Using MCP Clients + +### Tool Execution + +Tools are server-side functions that clients can discover and execute. The MCP client provides methods to list available tools and execute them with specific parameters. Each tool has a unique name and accepts a map of parameters. + +#### Sync API + +```java +// List available tools and their names +var tools = client.listTools(); +tools.forEach(tool -> System.out.println(tool.getName())); + +// Execute a tool with parameters +var result = client.callTool("calculator", Map.of( + "operation", "add", + "a", 1, + "b", 2 +)); +``` + +#### Async API + +```java +// List available tools asynchronously +client.listTools() + .doOnNext(tools -> tools.forEach(tool -> + System.out.println(tool.getName()))) + .subscribe(); + +// Execute a tool asynchronously +client.callTool("calculator", Map.of( + "operation", "add", + "a", 1, + "b", 2 + )) + .subscribe(); +``` + +### Resource Access + +Resources represent server-side data sources that clients can access using URI templates. The MCP client provides methods to discover available resources and retrieve their contents through a standardized interface. + +#### Sync API + +```java +// List available resources and their names +var resources = client.listResources(); +resources.forEach(resource -> System.out.println(resource.getName())); + +// Retrieve resource content using a URI template +var content = client.getResource("file", Map.of( + "path", "/path/to/file.txt" +)); +``` + +#### Async API + +```java +// List available resources asynchronously +client.listResources() + .doOnNext(resources -> resources.forEach(resource -> + System.out.println(resource.getName()))) + .subscribe(); + +// Retrieve resource content asynchronously +client.getResource("file", Map.of( + "path", "/path/to/file.txt" + )) + .subscribe(); +``` + +### Prompt System + +The prompt system enables interaction with server-side prompt templates. These templates can be discovered and executed with custom parameters, allowing for dynamic text generation based on predefined patterns. + +#### Sync API + +```java +// List available prompt templates +var prompts = client.listPrompts(); +prompts.forEach(prompt -> System.out.println(prompt.getName())); + +// Execute a prompt template with parameters +var response = client.executePrompt("echo", Map.of( + "text", "Hello, World!" +)); +``` + +#### Async API + +```java +// List available prompt templates asynchronously +client.listPrompts() + .doOnNext(prompts -> prompts.forEach(prompt -> + System.out.println(prompt.getName()))) + .subscribe(); + +// Execute a prompt template asynchronously +client.executePrompt("echo", Map.of( + "text", "Hello, World!" + )) + .subscribe(); +``` + +### Using Completion + +As part of the [Completion capabilities](https://modelcontextprotocol.io/specification/latest/server/utilities/completion), MCP provides a standardized way for servers to offer argument autocompletion suggestions for prompts and resource URIs. + +Check the [Server Completion capabilities](server.html#completion-specification) to learn how to enable and configure completions on the server side. + +On the client side, the MCP client provides methods to request auto-completions: + +#### Sync API + +```java + +CompleteRequest request = new CompleteRequest( + new PromptReference("code_review"), + new CompleteRequest.CompleteArgument("language", "py")); + +CompleteResult result = syncMcpClient.completeCompletion(request); + +``` + +#### Async API + +```java + +CompleteRequest request = new CompleteRequest( + new PromptReference("code_review"), + new CompleteRequest.CompleteArgument("language", "py")); + +Mono result = mcpClient.completeCompletion(request); + +``` + +### Adding context information + +HTTP request sent through SSE or Streamable HTTP transport can be customized with +dedicated APIs (see [client transport](#client-transport)). These customizers may need +additional context-specific information that must be injected at the client level. + +#### Sync API + +The `McpSyncClient` is used in a blocking environment, and may rely on thread-locals to share information. +For example, some frameworks store the current server request or security tokens in a thread-local. +To make this type of information available to underlying transports, use `SyncSpec#transportContextProvider`: + +```java +McpClient.sync(transport) + .transportContextProvider(() -> { + var data = obtainDataFromThreadLocals(); + return McpTransportContext.create( + Map.of("some-data", data) + ); + }) + .build(); +``` + +This `McpTransportContext` will be available in HttpClient-based `McpSyncHttpClientRequestCustomizer` +and WebClient-based `ExchangeFilterFunction`. + +#### Async API + +The `McpAsyncClient` is used in a reactive environment. +Information is passed through the reactive chain using the [Reactor context](https://projectreactor.io/docs/core/release/reference/advancedFeatures/context.html). +To insert data in the Reactor context, use `contextWrite` when calling client operations: + +```java +var client = McpClient.async(transport) + // ... + .build(); + +Mono toolList = client + .listTools() + .contextWrite(ctx -> { + var transportContext = McpTransportContext.create( + Map.of("some-key", "some value") + ); + return ctx.put(McpTransportContext.KEY, transportContext) + .put("reactor-context-key", "some reactor value"); + }); +``` + +With this, the `McpTransportContext` will be available in HttpClient-based `McpAsyncHttpClientRequestCustomizer`. +The entire Reactor context is also available in both `McpAsyncHttpClientRequestCustomizer` +and WebClient's `ExchangeFilterFunction`, through `Mono.deferContextual`: + +```java +WebClient.builder() + .filter((request, next) -> { + return Mono.deferContextual(ctx -> { + var transportContext = ctx.get(McpTransportContext.KEY); + var someReactorValue = ctx.get("reactor-context-key"); + // ... use context values ... + }); + }); +``` diff --git a/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/images/java-mcp-client-architecture.jpg b/mcp-core/src/main/javadoc/io/modelcontextprotocol/doc-files/images/java-mcp-client-architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..688a2b4ad0f92090bf0033ab5eb3b20fce1a8484 GIT binary patch literal 676694 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