[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/main/docs/server.md [Back]  [Original]

---
title: MCP Server
description: Learn how to implement and configure a Model Context Protocol (MCP) server
---

# MCP Server

## Overview

The MCP Server is a foundational component in the Model Context Protocol (MCP) architecture that provides tools, resources, and capabilities to clients. It implements the server-side of the protocol, responsible for:

- Exposing tools that clients can discover and execute
- Managing resources with URI-based access patterns and resource templates
- Providing prompt templates and handling prompt requests
- Supporting capability negotiation with clients
- Providing argument autocompletion suggestions (completions)
- Implementing server-side protocol operations
- Managing concurrent client connections
- Providing structured logging and notifications

!!! tip
    The core `io.modelcontextprotocol.sdk:mcp` module provides STDIO, SSE, and Streamable HTTP server transport implementations without requiring external web frameworks.

    Spring-specific transport implementations (`mcp-spring-webflux`, `mcp-spring-webmvc`) are now part of [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`) and are no longer shipped by this SDK.
    See the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-server-boot-starter-docs.html) documentation for Spring-based server setup.

The server supports both synchronous and asynchronous APIs, allowing for flexible integration in different application contexts.

=== "Sync API"

    ```java
    // Create a server with custom configuration
    McpSyncServer syncServer = McpServer.sync(transportProvider)
        .serverInfo("my-server", "1.0.0")
        .capabilities(ServerCapabilities.builder()
            .resources(false, true)  // Resource support: subscribe=false, listChanged=true
            .tools(true)             // Enable tool support with list changes
            .prompts(true)           // Enable prompt support with list changes
            .completions()           // Enable completions support
            .logging()               // Enable logging support
            .build())
        .build();

    // Register tools, resources, and prompts
    syncServer.addTool(syncToolSpecification);
    syncServer.addResource(syncResourceSpecification);
    syncServer.addPrompt(syncPromptSpecification);

    // Close the server when done
    syncServer.close();
    ```

=== "Async API"

    ```java
    // Create an async server with custom configuration
    McpAsyncServer asyncServer = McpServer.async(transportProvider)
        .serverInfo("my-server", "1.0.0")
        .capabilities(ServerCapabilities.builder()
            .resources(false, true)  // Resource support: subscribe=false, listChanged=true
            .tools(true)             // Enable tool support with list changes
            .prompts(true)           // Enable prompt support with list changes
            .completions()           // Enable completions support
            .logging()               // Enable logging support
            .build())
        .build();

    // Register tools, resources, and prompts
    asyncServer.addTool(asyncToolSpecification)
        .doOnSuccess(v -> logger.info("Tool registered"))
        .subscribe();

    asyncServer.addResource(asyncResourceSpecification)
        .doOnSuccess(v -> logger.info("Resource registered"))
        .subscribe();

    asyncServer.addPrompt(asyncPromptSpecification)
        .doOnSuccess(v -> logger.info("Prompt registered"))
        .subscribe();

    // Close the server when done
    asyncServer.close()
        .doOnSuccess(v -> logger.info("Server closed"))
        .subscribe();
    ```

### Server Types

The SDK supports multiple server creation patterns depending on your transport requirements:

```java
// Single-session server with SSE transport provider
McpSyncServer server = McpServer.sync(sseTransportProvider).build();

// Streamable HTTP server
McpSyncServer server = McpServer.sync(streamableTransportProvider).build();

// Stateless server (no session management)
McpSyncServer server = McpServer.sync(statelessTransport).build();
```

## Server Transport Providers

The transport layer in the MCP SDK is responsible for handling the communication between clients and servers.
It provides different implementations to support various communication protocols and patterns.
The SDK includes several built-in transport provider implementations:

### STDIO

Create process-based transport using stdin/stdout:

```java
StdioServerTransportProvider transportProvider =
    new StdioServerTransportProvider(McpJsonDefaults.getMapper());
```

Provides bidirectional JSON-RPC message handling over standard input/output streams with non-blocking message processing, serialization/deserialization, and graceful shutdown support.

Key features:

- Bidirectional communication through stdin/stdout
- Process-based integration support
- Simple setup and configuration
- Lightweight implementation

### Streamable HTTP

=== "Streamable HTTP Servlet"

    Creates a Servlet-based Streamable HTTP server transport. Included in the core `mcp` module:

    ```java
    HttpServletStreamableServerTransportProvider transportProvider =
        HttpServletStreamableServerTransportProvider.builder()
            .jsonMapper(jsonMapper)
            .mcpEndpoint("/mcp")
            .build();
    ```

    To use with a Spring Web application, register it as a Servlet bean:

    ```java
    @Configuration
    @EnableWebMvc
    public class McpServerConfig implements WebMvcConfigurer {

        @Bean
        public HttpServletStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
            return HttpServletStreamableServerTransportProvider.builder()
                .jsonMapper(jsonMapper)
                .mcpEndpoint("/mcp")
                .build();
        }

        @Bean
        public ServletRegistrationBean mcpServlet(
                HttpServletStreamableServerTransportProvider transportProvider) {
            return new ServletRegistrationBean(transportProvider);
        }
    }
    ```

    Key features:

    - Efficient bidirectional HTTP communication
    - Session management for multiple client connections
    - Keep-alive pings on sessions with an open stream, enabled by default every 30 minutes
      (`keepAliveInterval`, `null` to disable)
    - Eviction of idle sessions  no open stream and no request for a full interval  every
      30 minutes by default (`sessionSweepInterval`, `null` to keep sessions until deleted)
    - Security validation support
    - Graceful shutdown support

=== "Streamable HTTP WebFlux (external)"

    Creates WebFlux-based Streamable HTTP server transport. Requires the `mcp-spring-webflux` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):

    ```java
    @Configuration
    class McpConfig {
        @Bean
        WebFluxStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
            return WebFluxStreamableServerTransportProvider.builder()
                .jsonMapper(jsonMapper)
                .messageEndpoint("/mcp")
                .build();
        }

        @Bean
        RouterFunction mcpRouterFunction(
                WebFluxStreamableServerTransportProvider transportProvider) {
            return transportProvider.getRouterFunction();
        }
    }
    ```

    Key features:

    - Reactive HTTP streaming with WebFlux
    - Concurrent client connections
    - Configurable keep-alive intervals
    - Security validation support

=== "Streamable HTTP WebMvc (external)"

    Creates WebMvc-based Streamable HTTP server transport. Requires the `mcp-spring-webmvc` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):

    ```java
    @Configuration
    @EnableWebMvc
    class McpConfig {
        @Bean
        WebMvcStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
            return WebMvcStreamableServerTransportProvider.builder()
                .jsonMapper(jsonMapper)
                .mcpEndpoint("/mcp")
                .build();
        }

        @Bean
        RouterFunction mcpRouterFunction(
                WebMvcStreamableServerTransportProvider transportProvider) {
            return transportProvider.getRouterFunction();
        }
    }
    ```

### SSE HTTP (Legacy)

=== "SSE Servlet"

    Creates a Servlet-based SSE server transport. Included in the core `mcp` module.
    The `HttpServletSseServerTransportProvider` can be used with any Servlet container.
    To use it with a Spring Web application, you can register it as a Servlet bean:

    ```java
    @Configuration
    @EnableWebMvc
    public class McpServerConfig implements WebMvcConfigurer {

        @Bean
        public HttpServletSseServerTransportProvider servletSseServerTransportProvider() {
            return HttpServletSseServerTransportProvider.builder()
                .messageEndpoint("/mcp/message")
                .build();
        }

        @Bean
        public ServletRegistrationBean customServletBean(
                HttpServletSseServerTransportProvider transportProvider) {
            return new ServletRegistrationBean(transportProvider);
        }
    }
    ```

    Implements the MCP HTTP with SSE transport specification using the traditional Servlet API, providing:

    - Asynchronous message handling using Servlet 6.0 async support
    - Session management for multiple client connections
    - Two types of endpoints:
        - SSE endpoint (`/sse`) for server-to-client events
        - Message endpoint (configurable) for client-to-server requests
    - Error handling and response formatting
    - Graceful shutdown support

=== "SSE WebFlux (external)"

    Creates WebFlux-based SSE server transport. Requires the `mcp-spring-webflux` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):

    ```java
    @Configuration
    class McpConfig {
        @Bean
        WebFluxSseServerTransportProvider webFluxSseServerTransportProvider(ObjectMapper mapper) {
            return new WebFluxSseServerTransportProvider(mapper, "/mcp/message");
        }

        @Bean
        RouterFunction mcpRouterFunction(WebFluxSseServerTransportProvider transportProvider) {
            return transportProvider.getRouterFunction();
        }
    }
    ```

    Implements the MCP HTTP with SSE transport specification, providing:

    - Reactive HTTP streaming with WebFlux
    - Concurrent client connections through SSE endpoints
    - Message routing and session management
    - Graceful shutdown capabilities

=== "SSE WebMvc (external)"

    Creates WebMvc-based SSE server transport. Requires the `mcp-spring-webmvc` dependency from [Spring AI](https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/mcp/mcp-overview.html) 2.0+ (group `org.springframework.ai`):

    ```java
    @Configuration
    @EnableWebMvc
    class McpConfig {
        @Bean
        WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(ObjectMapper mapper) {
            return new WebMvcSseServerTransportProvider(mapper, "/mcp/message");
        }

        @Bean
        RouterFunction mcpRouterFunction(
                WebMvcSseServerTransportProvider transportProvider) {
            return transportProvider.getRouterFunction();
        }
    }
    ```

    Implements the MCP HTTP with SSE transport specification, providing:

    - Server-side event streaming
    - Integration with Spring WebMVC
    - Support for traditional web applications
    - Synchronous operation handling


## Server Capabilities

The server can be configured with various capabilities:

```java
var capabilities = ServerCapabilities.builder()
    .resources(true, true)   // Resource support: subscribe=true, listChanged=true
    .tools(true)             // Tool support with list changes notifications
    .prompts(true)           // Prompt support with list changes notifications
    .completions()           // Enable completions support
    .logging()               // Enable logging support
    .build();
```

### Tool Specification

The Model Context Protocol allows servers to [expose tools](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/) that can be invoked by language models.
The Java SDK allows implementing Tool Specifications with their handler functions.
Tools enable AI models to perform calculations, access external APIs, query databases, and manipulate files.

The recommended approach is to use the builder pattern and `CallToolRequest` as the handler parameter:

=== "Sync"

    ```java
    // Sync tool specification using builder
    var syncToolSpecification = SyncToolSpecification.builder()
        .tool(Tool.builder("calculator", schema)
            .description("Basic calculator")
            .build())
        .callHandler((exchange, request) -> {
            // Access arguments via request.arguments()
            String operation = (String) request.arguments().get("operation");
            int a = (int) request.arguments().get("a");
            int b = (int) request.arguments().get("b");
            // Tool implementation
            return CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent("Result: " + result)))
                .build();
        })
        .build();
    ```

=== "Async"

    ```java
    // Async tool specification using builder
    var asyncToolSpecification = AsyncToolSpecification.builder()
        .tool(Tool.builder("calculator", schema)
            .description("Basic calculator")
            .build())
        .callHandler((exchange, request) -> {
            // Access arguments via request.arguments()
            String operation = (String) request.arguments().get("operation");
            int a = (int) request.arguments().get("a");
            int b = (int) request.arguments().get("b");
            // Tool implementation
            return Mono.just(CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent("Result: " + result)))
                .build());
        })
        .build();
    ```

The Tool specification includes a Tool definition with `name`, `description`, and `inputSchema` followed by a call handler that implements the tool's logic.
The handler receives `McpSyncServerExchange`/`McpAsyncServerExchange` for client interaction and a `CallToolRequest` containing the tool arguments.

You can also register tools directly on the server builder using the `toolCall` convenience method:

```java
var server = McpServer.sync(transportProvider)
    .toolCall(
        Tool.builder("echo", schema).description("Echoes input").build(),
        (exchange, request) -> CallToolResult.builder()
            .content(List.of(new McpSchema.TextContent(request.arguments().get("text").toString())))
            .build()
    )
    .build();
```

#### Tool Input Validation

By default the server validates incoming tool arguments against the tool's `inputSchema` before invoking the handler. When validation fails, the call returns a `CallToolResult` with `isError` set and a textual error, rather than reaching your handler. Validation uses the configured `JsonSchemaValidator` (or the default from `McpJsonDefaults.getSchemaValidator()`), and can be turned off on the server builder:

```java
var server = McpServer.sync(transportProvider)
    .validateToolInputs(false)   // default is true
    .build();
```

The embedded JSON Schema documents themselves (`Tool.inputSchema`, `Tool.outputSchema`, and elicitation `requestedSchema`) are validated against the JSON Schema 2020-12 meta-schema (SEP-1613). Malformed schemas are rejected at build time (`McpServer.build()`) and when calling `addTool()`, throwing an `IllegalArgumentException` that names the offending field. A schema that declares a different dialect via `$schema` is accepted without meta-schema validation.

#### Tool Result Content Types

Besides `TextContent`, a `CallToolResult` can return images, audio, and embedded resources  any combination of these can appear in the same result's `content` list:

```java
var syncToolSpecification = SyncToolSpecification.builder()
    .tool(Tool.builder("generate-report", schema)
        .description("Generates a report with mixed content")
        .build())
    .callHandler((exchange, request) -> {
        var text = TextContent.builder("Report summary:").build();

        // Image content: base64-encoded data + MIME type
        var image = ImageContent.builder(base64PngData, "image/png").build();

        // Audio content: base64-encoded data + MIME type
        var audio = AudioContent.builder(base64WavData, "audio/wav").build();

        // Embedded resource: wraps a TextResourceContents or BlobResourceContents
        var resourceContents = TextResourceContents.builder("report://details", "Full details...")
            .mimeType("text/plain")
            .build();
        var embeddedResource = EmbeddedResource.builder(resourceContents).build();

        return CallToolResult.builder()
            .content(List.of(text, image, audio, embeddedResource))
            .isError(false)
            .build();
    })
    .build();
```

`ImageContent.builder(data, mimeType)` and `AudioContent.builder(data, mimeType)` both take base64-encoded binary data. `EmbeddedResource.builder(resourceContents)` wraps either a `TextResourceContents` (for text data) or a `BlobResourceContents` (for base64-encoded binary data)  see [Reading Binary Resources](#reading-binary-resources) for the `BlobResourceContents` shape.

### Filtering the Tool Listing per Request

By default every registered tool is advertised to every caller. Over an HTTP transport you can
vary the `tools/list` response per request  to hide tools the caller is not authorized to see,
or to trim a large catalog down to a relevant subset  by registering one or more tool filters.

The filter receives the `McpTransportContext` extracted from the current request, so it can key
on HTTP headers, a token, a resolved principal, or anything else your
`contextExtractor` puts there.

=== "Sync"

    ```java
    McpServer.sync(transportProvider)
        .tools(publicTool, adminTool)
        .addToolFilter((transportContext, tool) ->
            !tool.name().startsWith("admin-") || isAdmin(transportContext))
        .build();
    ```

=== "Async"

    ```java
    McpServer.async(transportProvider)
        .tools(publicTool, adminTool)
        .addToolFilter((transportContext, tool) -> {
            if (!tool.name().startsWith("admin-")) {
                return Mono.just(true);
            }
            return isAdmin(transportContext); // Mono
        })
        .build();
    ```

The same `addToolFilter(...)` method is available on the stateless builders.

!!! warning "Hiding a tool does not make it unreachable"

    The filter controls **advertisement only**. A hidden tool called by name still executes:
    you MUST enforce permissions in the tool's call handler. Use the filter to control what a
    caller is told about, not what they are allowed to do.

**Evaluation semantics**

- The filter is consulted on **every** listing request and never cached, so the same session may
  legitimately see different results for two successive requests carrying different credentials.
- Registration order is preserved; only omissions happen.
- Returning `Mono.empty()` from an async filter omits the tool. An error fails the whole listing
  request rather than silently hiding tools: a client cannot tell a filtered-down listing from a
  partial one, and MCP has no way to signal "this listing was incomplete, retry".
- A filter that errors is logged server-side and reported to the client as an opaque
  `-32603 Internal error` with no `data`. If you want the client to see a specific error, throw an
  `McpError`, those are passed through.
- Filters accumulate as a boolean **AND**: a tool is listed only when every registered filter accepts it, so a
  later `addToolFilter(...)` can never widen access. Evaluation follows registration order and
  short-circuits on the first filter that hides a tool.
- `toolFilters(Consumer)` hands you the list of filters registered so far, so you can
  inspect, reorder or clear them before building  useful when filters come from several places:

    ```java
    McpServer.sync(transportProvider)
        .addToolFilter(tenantFilter)
        .toolFilters(filters -> filters.add(0, cheapDenyAllForAnonymousFilter))
        .build();
    ```

- Tools are tested one at a time, so a filter that performs I/O per tool costs one round trip per
  tool. Sync filters also run on a shared scheduler thread  not the request thread  unless
  `immediateExecution(true)` is set, so thread-bound request state (Spring Security's
  `SecurityContextHolder`, MDC, custom `ThreadLocal` holders) is **not visible** inside the filter.
  For both reasons, resolve per-request state **once** in the transport's `contextExtractor`,
  which does run on the request thread, and read only the extracted context in the filter:

    ```java
    // transport builder: one authorization lookup, on the request thread,
    // shared by every tool tested in this request
    var transportProvider = HttpServletStreamableServerTransportProvider.builder()
        .contextExtractor(request -> McpTransportContext.create(
                Map.of("perms", introspect(request.getHeader("Authorization")))))
        // ...
        .build();

    // server builder: the filter reads only the extracted context
    McpServer.sync(transportProvider)
        .addToolFilter((context, tool) ->
                ((Set) context.get("perms")).contains(tool.name()))
        .build();
    ```

- `notifications/tools/list_changed` is **not** filtered. It is a server-initiated broadcast with
  no request in flight, so there is no context to evaluate. A client may be told something changed
  when its own visible set did not; it gets the correct view on its next `tools/list`. Consider disabling
  this notification entirely when using tool filters.
- With STDIO there is no per-request metadata, so the filter receives `McpTransportContext.EMPTY` and has nothing to key
  on.

### Resource Specification

Specification of a resource with its handler function.
Resources provide context to AI models by exposing data such as: File contents, Database records, API responses, System information, Application state.

=== "Sync"

    ```java
    // Sync resource specification
    var syncResourceSpecification = new McpServerFeatures.SyncResourceSpecification(
        Resource.builder("custom://resource", "name")
            .description("description")
            .mimeType("text/plain")
            .build(),
        (exchange, request) -> {
            // Resource read implementation
            return ReadResourceResult.builder(contents).build();
        }
    );
    ```

=== "Async"

    ```java
    // Async resource specification
    var asyncResourceSpecification = new McpServerFeatures.AsyncResourceSpecification(
        Resource.builder("custom://resource", "name")
            .description("description")
            .mimeType("text/plain")
            .build(),
        (exchange, request) -> {
            // Resource read implementation
            return Mono.just(ReadResourceResult.builder(contents).build());
        }
    );
    ```

#### Reading Binary Resources

Binary resources (images, PDFs, audio, etc.) are returned as `BlobResourceContents`, which carries base64-encoded data instead of the plain `text` field used by `TextResourceContents`:

```java
var binaryResourceSpecification = new McpServerFeatures.SyncResourceSpecification(
    Resource.builder("file:///logo.png", "Logo")
        .description("Application logo")
        .mimeType("image/png")
        .build(),
    (exchange, request) -> {
        String base64Data = Base64.getEncoder().encodeToString(readLogoBytes());
        return ReadResourceResult.builder(List.of(
                BlobResourceContents.builder(request.uri(), base64Data)
                    .mimeType("image/png")
                    .build()))
            .build();
    }
);
```

`ReadResourceResult` accepts a list mixing `TextResourceContents` and `BlobResourceContents`, so a single resource read can return multiple representations if needed.

### Resource Subscriptions

When the `subscribe` capability is enabled, clients can subscribe to specific resources and receive targeted `notifications/resources/updated` notifications when those resources change. Only sessions that have explicitly subscribed to a given URI receive the notification  not every connected client.

Enable subscription support in the server capabilities:

```java
McpSyncServer server = McpServer.sync(transportProvider)
    .serverInfo("my-server", "1.0.0")
    .capabilities(ServerCapabilities.builder()
        .resources(true, false)  // subscribe=true, listChanged=false
        .build())
    .resources(myResourceSpec)
    .build();
```

When a subscribed resource changes, notify only the interested sessions:

=== "Sync"

    ```java
    server.notifyResourcesUpdated(
        new McpSchema.ResourcesUpdatedNotification("custom://resource")
    );
    ```

=== "Async"

    ```java
    server.notifyResourcesUpdated(
        new McpSchema.ResourcesUpdatedNotification("custom://resource")
    ).subscribe();
    ```

If no sessions are subscribed to the given URI the call completes immediately without sending any messages. Subscription state is automatically cleaned up when a client session closes.

### Resource Template Specification

Resource templates allow servers to expose parameterized resources using URI templates:

```java
// Resource template specification
var resourceTemplateSpec = new McpServerFeatures.SyncResourceTemplateSpecification(
    ResourceTemplate.builder("file://{path}", "File Resource")
        .description("Access files by path")
        .mimeType("application/octet-stream")
        .build(),
    (exchange, request) -> {
        // Read the file at the requested URI
        return ReadResourceResult.builder(contents).build();
    }
);
```

### Prompt Specification

As part of the [Prompting capabilities](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/), MCP provides a standardized way for servers to expose prompt templates to clients.
The Prompt Specification is a structured template for AI model interactions that enables consistent message formatting, parameter substitution, context injection, response formatting, and instruction templating.

=== "Sync"

    ```java
    // Sync prompt specification
    var syncPromptSpecification = new McpServerFeatures.SyncPromptSpecification(
        Prompt.builder("greeting")
            .description("description")
            .arguments(List.of(
                PromptArgument.builder("name")
                    .description("description")
                    .required(true)
                    .build()
            ))
            .build(),
        (exchange, request) -> {
            // Prompt implementation
            return GetPromptResult.builder(messages).description(description).build();
        }
    );
    ```

=== "Async"

    ```java
    // Async prompt specification
    var asyncPromptSpecification = new McpServerFeatures.AsyncPromptSpecification(
        Prompt.builder("greeting")
            .description("description")
            .arguments(List.of(
                PromptArgument.builder("name")
                    .description("description")
                    .required(true)
                    .build()
            ))
            .build(),
        (exchange, request) -> {
            // Prompt implementation
            return Mono.just(GetPromptResult.builder(messages).description(description).build());
        }
    );
    ```

The prompt definition includes name (identifier for the prompt), description (purpose of the prompt), and list of arguments (parameters for templating).
The handler function processes requests and returns formatted templates.
The first argument is `McpSyncServerExchange`/`McpAsyncServerExchange` for client interaction, and the second argument is a `GetPromptRequest` instance.

#### Prompts with Embedded Resources and Images

A prompt's messages can carry `EmbeddedResource` or `ImageContent` instead of plain text by passing them as the `content` argument of `PromptMessage.builder(role, content)`:

```java
// Prompt that embeds a resource's content in one of its messages
var promptWithResource = new McpServerFeatures.SyncPromptSpecification(
    Prompt.builder("review-file")
        .description("Reviews a file, embedding its content in the prompt")
        .arguments(List.of(PromptArgument.builder("resourceUri").required(true).build()))
        .build(),
    (exchange, request) -> {
        String resourceUri = (String) request.arguments().get("resourceUri");
        var resourceContents = TextResourceContents.builder(resourceUri, loadFileContent(resourceUri))
            .mimeType("text/plain")
            .build();
        var embeddedResource = EmbeddedResource.builder(resourceContents).build();

        return GetPromptResult.builder(List.of(
                PromptMessage.builder(Role.USER, embeddedResource).build(),
                PromptMessage.builder(Role.USER, TextContent.builder("Please review the file above.").build()).build()))
            .build();
    }
);

// Prompt that embeds an image in one of its messages
var promptWithImage = new McpServerFeatures.SyncPromptSpecification(
    Prompt.builder("describe-image")
        .description("Asks the model to describe an embedded image")
        .arguments(List.of())
        .build(),
    (exchange, request) -> GetPromptResult.builder(List.of(
            PromptMessage.builder(Role.USER, ImageContent.builder(base64PngData, "image/png").build()).build(),
            PromptMessage.builder(Role.USER, TextContent.builder("Describe the image above.").build()).build()))
        .build()
);
```

### Completion Specification

Completions allow servers to provide argument autocompletion suggestions for prompts and resources:

=== "Sync"

    ```java
    // Sync completion specification
    var syncCompletionSpec = new McpServerFeatures.SyncCompletionSpecification(
        new McpSchema.PromptReference("greeting"),  // Reference to a prompt
        (exchange, request) -> {
            String argName = request.argument().name();
            String partial = request.argument().value();
            // Return matching suggestions
            List suggestions = findMatches(partial);
            return new McpSchema.CompleteResult(
                new McpSchema.CompleteResult.CompleteCompletion(suggestions, suggestions.size(), false)
            );
        }
    );
    ```

=== "Async"

    ```java
    // Async completion specification
    var asyncCompletionSpec = new McpServerFeatures.AsyncCompletionSpecification(
        new McpSchema.PromptReference("greeting"),
        (exchange, request) -> {
            String argName = request.argument().name();
            String partial = request.argument().value();
            List suggestions = findMatches(partial);
            return Mono.just(new McpSchema.CompleteResult(
                new McpSchema.CompleteResult.CompleteCompletion(suggestions, suggestions.size(), false)
            ));
        }
    );
    ```

Completions can be registered for both `PromptReference` and `ResourceReference` types. A `ResourceReference` completion suggests values for a resource template's URI parameters instead of a prompt's arguments:

=== "Sync"

    ```java
    // Sync completion specification for a resource template argument
    var syncResourceCompletionSpec = new McpServerFeatures.SyncCompletionSpecification(
        new McpSchema.ResourceReference("file://{path}"),  // Reference to a resource template
        (exchange, request) -> {
            String argName = request.argument().name();
            String partial = request.argument().value();
            // Return matching suggestions, e.g. matching file paths
            List suggestions = findMatchingPaths(partial);
            return new McpSchema.CompleteResult(
                new McpSchema.CompleteResult.CompleteCompletion(suggestions, suggestions.size(), false)
            );
        }
    );
    ```

=== "Async"

    ```java
    // Async completion specification for a resource template argument
    var asyncResourceCompletionSpec = new McpServerFeatures.AsyncCompletionSpecification(
        new McpSchema.ResourceReference("file://{path}"),
        (exchange, request) -> {
            String argName = request.argument().name();
            String partial = request.argument().value();
            List suggestions = findMatchingPaths(partial);
            return Mono.just(new McpSchema.CompleteResult(
                new McpSchema.CompleteResult.CompleteCompletion(suggestions, suggestions.size(), false)
            ));
        }
    );
    ```

### Using Sampling from a Server

To use [Sampling capabilities](https://spec.modelcontextprotocol.io/specification/2024-11-05/client/sampling/), connect to a client that supports sampling.
No special server configuration is needed, but verify client sampling support before making requests.
Learn about [client sampling support](client.md#sampling-support).

Once connected to a compatible client, the server can request language model generations:

=== "Sync API"

    ```java
    // Create a server
    McpSyncServer server = McpServer.sync(transportProvider)
        .serverInfo("my-server", "1.0.0")
        .build();

    // Define a tool that uses sampling
    var calculatorTool = SyncToolSpecification.builder()
        .tool(Tool.builder("ai-calculator", schema)
            .description("Performs calculations using AI")
            .build())
        .callHandler((exchange, request) -> {
            // Check if client supports sampling
            if (exchange.getClientCapabilities().sampling() == null) {
                return CallToolResult.builder()
                    .content(List.of(new McpSchema.TextContent("Client does not support AI capabilities")))
                    .build();
            }

            // Create a sampling request
            CreateMessageRequest samplingRequest = CreateMessageRequest.builder(
                    List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
                        new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))),
                    100)
                .modelPreferences(McpSchema.ModelPreferences.builder()
                    .hints(List.of(
                        McpSchema.ModelHint.of("claude-3-sonnet"),
                        McpSchema.ModelHint.of("claude")
                    ))
                    .intelligencePriority(0.8)
                    .speedPriority(0.5)
                    .build())
                .systemPrompt("You are a helpful calculator assistant. Provide only the numerical answer.")
                .build();

            // Request sampling from the client
            CreateMessageResult result = exchange.createMessage(samplingRequest);

            // Process the result
            String answer = ((McpSchema.TextContent) result.content()).text();
            return CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent(answer)))
                .build();
        })
        .build();

    // Add the tool to the server
    server.addTool(calculatorTool);
    ```

=== "Async API"

    ```java
    // Create a server
    McpAsyncServer server = McpServer.async(transportProvider)
        .serverInfo("my-server", "1.0.0")
        .build();

    // Define a tool that uses sampling
    var calculatorTool = AsyncToolSpecification.builder()
        .tool(Tool.builder("ai-calculator", schema)
            .description("Performs calculations using AI")
            .build())
        .callHandler((exchange, request) -> {
            // Check if client supports sampling
            if (exchange.getClientCapabilities().sampling() == null) {
                return Mono.just(CallToolResult.builder()
                    .content(List.of(new McpSchema.TextContent("Client does not support AI capabilities")))
                    .build());
            }

            // Create a sampling request
            CreateMessageRequest samplingRequest = CreateMessageRequest.builder(
                    List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
                        new McpSchema.TextContent("Calculate: " + request.arguments().get("expression")))),
                    100)
                .modelPreferences(McpSchema.ModelPreferences.builder()
                    .hints(List.of(
                        McpSchema.ModelHint.of("claude-3-sonnet"),
                        McpSchema.ModelHint.of("claude")
                    ))
                    .intelligencePriority(0.8)
                    .speedPriority(0.5)
                    .build())
                .systemPrompt("You are a helpful calculator assistant. Provide only the numerical answer.")
                .build();

            // Request sampling from the client
            return exchange.createMessage(samplingRequest)
                .map(result -> {
                    String answer = ((McpSchema.TextContent) result.content()).text();
                    return CallToolResult.builder()
                        .content(List.of(new McpSchema.TextContent(answer)))
                        .build();
                });
        })
        .build();

    // Add the tool to the server
    server.addTool(calculatorTool)
        .subscribe();
    ```

The `CreateMessageRequest` object allows you to specify: `Content` - the input text or image for the model,
`Model Preferences` - hints and priorities for model selection, `System Prompt` - instructions for the model's behavior and
`Max Tokens` - maximum length of the generated response.

### Using Elicitation from a Server

Servers can request user input from connected clients that support elicitation:

```java
var tool = SyncToolSpecification.builder()
    .tool(Tool.builder("confirm-action", schema)
        .description("Confirms an action with the user")
        .build())
    .callHandler((exchange, request) -> {
        // Check if client supports elicitation
        if (exchange.getClientCapabilities().elicitation() == null) {
            return CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent("Client does not support elicitation")))
                .build();
        }

        // Request user confirmation
        ElicitRequest elicitRequest = ElicitFormRequest.builder("Do you want to proceed with this action?", Map.of(
                "type", "object",
                "properties", Map.of("confirmed", Map.of("type", "boolean"))
            ))
            .build();

        ElicitResult result = exchange.elicit(elicitRequest);

        if (result.action() == ElicitResult.Action.ACCEPT) {
            // User accepted
            return CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent("Action confirmed")))
                .build();
        } else {
            return CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent("Action declined")))
                .build();
        }
    })
    .build();
```

To request out-of-band URL elicitation, such as a user authorizing an OAuth flow:

```java
var urlTool = SyncToolSpecification.builder()
    .tool(Tool.builder("oauth-auth", schema)
        .description("Authenticates via OAuth")
        .build())
    .callHandler((exchange, request) -> {
        // Request URL elicitation from client
        if (
                exchange.getClientCapabilities().elicitation() != null 
                && exchange.getClientCapabilities().elicitation().url() != null
        ) {
            ElicitRequest urlRequest = McpSchema.ElicitUrlRequest
                    .builder("Please authenticate", "https://example.com/oauth", "oauth-123").build();
            ElicitResult result = exchange.elicit(urlRequest);
            // handle result.action == CANCELLED or DENIED
            if (result.action() != ElicitResult.Action.ACCEPT) {
                return CallToolResult.builder()
                        .content(List.of(new McpSchema.TextContent("Authentication failed or cancelled")))
                        .build();
            }
        }

        // wait for user to visit the URL
        return CallToolResult.builder()
                .content(List.of(new McpSchema.TextContent("Authentication successful")))
                .build();
    })
    .build();
```

#### Elicitation with Enum Values (SEP-1330)

For form elicitation, the SDK provides typed helpers to build `requestedSchema` properties that render as single-select or multi-select choices, with or without human-readable titles:

```java
// Untitled single-select: plain enum values, no separate display titles
var untitledSingle = UntitledSingleSelectEnumSchema.builder()
    .enumValues("small", "medium", "large")
    .build();

// Titled single-select: value/title pairs via oneOf+const
var titledSingle = TitledSingleSelectEnumSchema.builder()
    .oneOf(new EnumSchemaOption("sm", "Small"),
           new EnumSchemaOption("md", "Medium"),
           new EnumSchemaOption("lg", "Large"))
    .build();

// Untitled multi-select: an array property whose items are an enum
var untitledMulti = UntitledMultiSelectEnumSchema
    .builder(UntitledMultiSelectItems.builder().enumValues("red", "green", "blue").build())
    .build();

// Titled multi-select: array items with value/title pairs via anyOf+const
var titledMulti = TitledMultiSelectEnumSchema
    .builder(TitledMultiSelectItems.builder()
        .anyOf(new EnumSchemaOption("red", "Red"), new EnumSchemaOption("green", "Green"))
        .build())
    .build();

// Convert the typed schema helpers into the plain Map shape
// expected by ElicitRequest.builder(message, requestedSchema)
var mapper = McpJsonDefaults.getMapper();
TypeRef mapType = new TypeRef() {};
Map requestedSchema = Map.of("type", "object", "properties",
        Map.of("size", mapper.convertValue(titledSingle, mapType),
               "colors", mapper.convertValue(titledMulti, mapType)),
        "required", List.of("size", "colors"));

ElicitRequest elicitRequest = ElicitRequest.builder("Choose your options", requestedSchema).build();
ElicitResult result = exchange.createElicitation(elicitRequest);
```

`LegacyTitledEnumSchema` (a value list plus a parallel `enumNames` list, built with `.enumValues(...)` and `.enumNames(...)`) is also available for clients that predate SEP-1330's `oneOf`/`anyOf` convention, but new schemas should prefer `TitledSingleSelectEnumSchema` / `TitledMultiSelectEnumSchema`.

### Pinging the Client

A server can check that a connected client is still responsive by sending a `ping` request through the exchange:

```java
var tool = SyncToolSpecification.builder()
    .tool(Tool.builder("health-check", emptyJsonSchema).description("Verifies the client is reachable").build())
    .callHandler((exchange, request) -> {
        exchange.ping(); // blocks until the client responds, or the request times out
        return CallToolResult.builder()
            .content(List.of(new McpSchema.TextContent("Client is responsive")))
            .build();
    })
    .build();
```

The async equivalent, `McpAsyncServerExchange.ping()`, returns a `Mono` that completes when the client responds. Ping requests are subject to the same `requestTimeout` configured on the server builder.

### Logging Support

The server provides structured logging capabilities that allow sending log messages to clients with different severity levels.
Log notifications can only be sent from within an existing client session, such as tools, resources, and prompts calls.

The server can send log messages using the `McpAsyncServerExchange`/`McpSyncServerExchange` object in the tool/resource/prompt handler function:

```java
var tool = AsyncToolSpecification.builder()
    .tool(Tool.builder("logging-test", emptyJsonSchema).description("Test logging notifications").build())
    .callHandler((exchange, request) ->
        exchange.loggingNotification( // Use the exchange to send log messages
            McpSchema.LoggingMessageNotification.builder(McpSchema.LoggingLevel.DEBUG, "Debug message")
              .logger("test-logger")
              .build())
          .then(Mono.just(CallToolResult.builder()
              .content(List.of(new McpSchema.TextContent("Logging test completed")))
              .build())))
    .build();

var mcpServer = McpServer.async(mcpServerTransportProvider)
  .serverInfo("test-server", "1.0.0")
  .capabilities(
    ServerCapabilities.builder()
      .logging() // Enable logging support
      .tools(true)
      .build())
  .tools(tool)
  .build();
```

On the client side, you can register a logging consumer to receive log messages from the server:

```java
var mcpClient = McpClient.sync(transport)
        .loggingConsumer(notification -> {
            System.out.println("Received log message: " + notification.data());
        })
        .build();

mcpClient.initialize();
mcpClient.setLoggingLevel(McpSchema.LoggingLevel.INFO);
```

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)

## Error Handling

The SDK provides comprehensive error handling through the McpError class, covering protocol compatibility, transport communication, JSON-RPC messaging, tool execution, resource management, prompt handling, timeouts, and connection issues. This unified error handling approach ensures consistent and reliable error management across both synchronous and asynchronous operations.

### Error Handling in Tool Implementations

#### Two Tiers of Errors

MCP distinguishes between two categories of errors in tool execution:

**1. Tool-Level Errors (Recoverable by the LLM)**

Use `CallToolResult` with `isError(true)` for validation failures, missing arguments, or domain errors the LLM can act on and retry.

```java
// Example: Domain validation failure (e.g., invalid email format)
if (!emailAddress.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
        return CallToolResult.builder()
        .content(List.of(new McpSchema.TextContent("Invalid argument: 'email' must be a valid email address.")))
        .isError(true)
        .build();
}
```

The LLM receives this as part of the normal tool response and can self-correct in a subsequent interaction.

**2. Protocol-Level Errors (Unrecoverable)**

Uncaught exceptions from a tool handler are mapped to a JSON-RPC error response. Use this only for truly unexpected failures (e.g., infrastructure errors such as DB timeout), not for input validation.

```java
// This propagates as a JSON-RPC error  use sparingly
throw new McpError(McpSchema.ErrorCodes.INTERNAL_ERROR, "Unexpected failure");
```

#### Decision Guide

| Situation                          | Approach                              |
|------------------------------------|---------------------------------------|
| Domain validation failure          | `CallToolResult` with `isError=true`  |
| Infrastructure / unexpected error  | Throw `McpError` or let it propagate  |
| Partial success with a warning     | `CallToolResult` with warning in text |

Web Proxy Viewer  |  New URL  |  Original Page