MCP Client Integration

This guide covers integration with MCP client features like sampling, elicitation, and roots. These features allow your MCP server to request services from the client.

Sampling

Sampling allows the server to request LLM responses from the client. This is useful when your server needs to generate text, make decisions, or process information using an LLM.

Checking Sampling Support

Before using sampling, check if the client supports it. This is negotiated during the MCP connection initialization.

import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Get AI assistance")
    String getAssistance(Sampling sampling) {
        if (sampling.isSupported()) { (1)
            // Use sampling
            return "Sampling is available";
        } else {
            return "Sampling not supported by this client";
        }
    }
}
1 Check if the client supports the sampling capability.

Basic Sampling Request

Request an LLM response from the client:

import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.SamplingMessage;
import io.quarkiverse.mcp.server.SamplingRequest;
import io.quarkiverse.mcp.server.SamplingResponse;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Ask the AI a question")
    String askAI(String question, Sampling sampling) {
        if (!sampling.isSupported()) {
            return "Sampling not supported";
        }

        SamplingRequest request = sampling.requestBuilder() (1)
            .setMaxTokens(100) (2)
            .addMessage(SamplingMessage.withUserRole(question)) (3)
            .build();

        SamplingResponse response = request.sendAndAwait(); (4)
        return response.content().asText().text(); (5)
    }
}
1 Create a new sampling request builder.
2 Set the maximum number of tokens to generate (the LLM response).
3 Add a message with the user role.
4 Send the request and wait for the response (blocking).
5 Extract the text content from the response.

Async Sampling

For non-blocking operations, use the async API:

import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.SamplingMessage;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;

public class MyTools {

    @Tool(description = "Ask the AI asynchronously")
    Uni<String> askAIAsync(String question, Sampling sampling) {
        if (!sampling.isSupported()) {
            return Uni.createFrom().item("Sampling not supported");
        }

        return sampling.requestBuilder()
            .setMaxTokens(100)
            .addMessage(SamplingMessage.withUserRole(question))
            .build()
            .send() (1)
            .map(response -> response.content().asText().text()); (2)
    }
}
1 Send the request asynchronously, returns a Uni<SamplingResponse>.
2 Map the response to extract the text content.

Advanced Sampling Options

Configure temperature, system prompt, and other parameters:

import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.SamplingMessage;
import io.quarkiverse.mcp.server.SamplingRequest.IncludeContext;
import io.quarkiverse.mcp.server.Tool;
import java.math.BigDecimal;
import java.time.Duration;

public class MyTools {

    @Tool(description = "Advanced sampling example")
    String advancedSampling(Sampling sampling) {
        if (!sampling.isSupported()) {
            return "Not supported";
        }

        return sampling.requestBuilder()
            .setMaxTokens(200)
            .setTemperature(BigDecimal.valueOf(0.7)) (1)
            .setSystemPrompt("You are a helpful coding assistant.") (2)
            .addMessage(SamplingMessage.withUserRole("Explain async programming"))
            .setIncludeContext(IncludeContext.THIS_SERVER) (3)
            .setTimeout(Duration.ofSeconds(30)) (4)
            .build()
            .sendAndAwait()
            .content().asText().text();
    }
}
1 Set the temperature for response randomness (0.0-1.0).
2 Provide a system prompt to guide the LLM’s behavior.
3 Include context from this server in the request.
4 Set a custom timeout for the request.

Multi-turn Conversations

Build conversations with multiple messages:

import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.SamplingMessage;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Multi-turn conversation")
    String conversation(Sampling sampling) {
        if (!sampling.isSupported()) {
            return "Not supported";
        }

        return sampling.requestBuilder()
            .setMaxTokens(150)
            // Recreate the conversation history
            .addMessage(SamplingMessage.withUserRole("What is Java?")) (1)
            .addMessage(SamplingMessage.withAssistantRole(
                "Java is a popular object-oriented programming language.")) (2)
            .addMessage(SamplingMessage.withUserRole(
                "What are its main features?")) (3)
            .build()
            .sendAndAwait()
            .content().asText().text();
    }
}
1 First user message.
2 Assistant response to provide context.
3 Follow-up user question.

Elicitation

Elicitation allows the server to request additional information from the client. It supports two modes:

  • Form mode — collects structured data from users with a form-like interface.

  • URL mode — directs users to an external URL for sensitive interactions (API keys, OAuth, payments) that must not pass through the MCP client.

Form Mode Elicitation

Form mode elicitation is used for collecting structured, non-sensitive input from users.

Checking Form Mode Support

Before using form mode elicitation, check if the client supports it. This is negotiated during the MCP connection initialization.

import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Request user input")
    String requestInfo(Elicitation elicitation) {
        if (elicitation.isFormModeSupported()) { (1)
            // Use form mode elicitation
            return "Form mode elicitation is available";
        } else {
            return "Form mode elicitation not supported by this client";
        }
    }
}
1 Check if the client supports the form mode of the elicitation capability.

Basic Form Mode Request

Request structured input from the user:

import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.ElicitationRequest;
import io.quarkiverse.mcp.server.ElicitationRequest.StringSchema;
import io.quarkiverse.mcp.server.ElicitationResponse;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Get user information")
    String getUserInfo(Elicitation elicitation) {
        if (!elicitation.isFormModeSupported()) {
            return "Form mode not supported";
        }

        ElicitationRequest request = elicitation.requestBuilder() (1)
            .setMessage("Please provide your information:") (2)
            .addSchemaProperty("username", new StringSchema(true)) (3)
            .addSchemaProperty("email", new StringSchema(true))
            .build();

        ElicitationResponse response = request.sendAndAwait(); (4)

        if (response.actionAccepted()) { (5)
            String username = response.content().getString("username"); (6)
            String email = response.content().getString("email");
            return "Hello " + username + " (" + email + ")";
        } else {
            return "User cancelled the request";
        }
    }
}
1 Create a new elicitation request builder.
2 Set the message to display to the user.
3 Add schema properties for the fields to collect (true = required).
4 Send the request and wait for the response.
5 Check if the user accepted or cancelled.
6 Extract the values from the response.

Schema Types

Form mode elicitation supports various schema types for different input types:

String Input
import io.quarkiverse.mcp.server.ElicitationRequest.StringSchema;

// Simple required string
new StringSchema(true)

// String with constraints
new StringSchema(
    "Email Address",           // title
    "Your email address",      // description
    50,                        // maxLength
    5,                         // minLength
    StringSchema.Format.EMAIL, // format validation
    true                       // required
)

// Using the builder
StringSchema.builder()
    .setTitle("Email Address")
    .setDescription("Your email address")
    .setMaxLength(50)
    .setMinLength(5)
    .setFormat(StringSchema.Format.EMAIL)
    .setRequired(true)
    .setDefaultValue("user@example.com")
    .build()
Number Input
import io.quarkiverse.mcp.server.ElicitationRequest.NumberSchema;

// Simple number
new NumberSchema(true)

// Number with range
new NumberSchema(
    "Age",          // title
    "Your age",     // description
    120,            // maximum
    0,              // minimum
    true            // required
)

// Using the builder
NumberSchema.builder()
    .setTitle("Age")
    .setDescription("Your age")
    .setMaximum(120)
    .setMinimum(0)
    .setRequired(true)
    .setDefaultValue(25)
    .build()
Integer Input
import io.quarkiverse.mcp.server.ElicitationRequest.IntegerSchema;

// Using the builder
IntegerSchema.builder()
    .setTitle("Quantity")
    .setDescription("Number of items")
    .setMaximum(100)
    .setMinimum(1)
    .setRequired(true)
    .setDefaultValue(1)
    .build()
Boolean Input
import io.quarkiverse.mcp.server.ElicitationRequest.BooleanSchema;

// Simple checkbox
new BooleanSchema(true)

// Boolean with metadata
new BooleanSchema(
    "Subscribe",                    // title
    "Subscribe to newsletter",      // description
    true,                          // defaultValue
    false                          // required
)

// Using the builder
BooleanSchema.builder()
    .setTitle("Subscribe")
    .setDescription("Subscribe to newsletter")
    .setDefaultValue(true)
    .setRequired(false)
    .build()
Enum/Select Input
import io.quarkiverse.mcp.server.ElicitationRequest.SingleSelectEnumSchema;
import java.util.List;

// Simple dropdown
new SingleSelectEnumSchema(List.of("small", "medium", "large"))

// Dropdown with titles
new SingleSelectEnumSchema(
    List.of("s", "m", "l"),                    // values
    List.of("Small", "Medium", "Large")        // display titles
)

// With default value
new SingleSelectEnumSchema(
    List.of("red", "green", "blue"),
    "blue"                                     // default
)

// Using the builder
SingleSelectEnumSchema.builder(List.of("s", "m", "l"))
    .setTitle("Size")
    .setDescription("Choose a size")
    .setEnumTitles(List.of("Small", "Medium", "Large"))
    .setRequired(true)
    .setDefaultValue("m")
    .build()
Multi-select Input
import io.quarkiverse.mcp.server.ElicitationRequest.MultiSelectEnumSchema;
import java.util.List;

// Multi-select checkbox group
new MultiSelectEnumSchema(
    List.of("java", "python", "javascript", "go")
)

// Using the builder
MultiSelectEnumSchema.builder(List.of("java", "python", "javascript", "go"))
    .setTitle("Languages")
    .setDescription("Select your programming languages")
    .setEnumTitles(List.of("Java", "Python", "JavaScript", "Go"))
    .setMinItems(1)
    .setMaxItems(3)
    .setRequired(true)
    .setDefaultValues(List.of("java"))
    .build()

Complete Form Mode Example

import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.ElicitationRequest.*;
import io.quarkiverse.mcp.server.Tool;
import java.util.List;

public class MyTools {

    @Tool(description = "User registration form")
    String registerUser(Elicitation elicitation) {
        if (!elicitation.isFormModeSupported()) {
            return "Not supported";
        }

        var response = elicitation.requestBuilder()
            .setMessage("Please fill out the registration form:")
            .addSchemaProperty("username",
                new StringSchema("Username", "Your username", 20, 3, null, true))
            .addSchemaProperty("email",
                new StringSchema("Email", "Your email", null, null,
                    StringSchema.Format.EMAIL, true))
            .addSchemaProperty("age",
                new NumberSchema("Age", "Your age", 120, 13, true))
            .addSchemaProperty("newsletter",
                new BooleanSchema("Newsletter", "Subscribe to updates", false, false))
            .addSchemaProperty("interests",
                new MultiSelectEnumSchema(
                    List.of("coding", "gaming", "music", "sports")))
            .build()
            .sendAndAwait();

        if (response.actionAccepted()) {
            return "Registration successful for " + response.content().getString("username");
        } else {
            return "Registration cancelled";
        }
    }
}

Async Form Mode Elicitation

For non-blocking operations:

import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.ElicitationRequest.StringSchema;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;

public class MyTools {

    @Tool(description = "Async elicitation")
    Uni<String> asyncElicitation(Elicitation elicitation) {
        if (!elicitation.isFormModeSupported()) {
            return Uni.createFrom().item("Not supported");
        }

        return elicitation.requestBuilder()
            .setMessage("Enter your name:")
            .addSchemaProperty("name", new StringSchema(true))
            .build()
            .send() (1)
            .map(response -> {
                if (response.actionAccepted()) {
                    return "Hello " + response.content().getString("name");
                } else {
                    return "Cancelled";
                }
            });
    }
}
1 Returns Uni<ElicitationResponse> for async processing.

URL Mode Elicitation

URL mode elicitation directs users to an external URL for out-of-band interactions. This is required for sensitive data like API keys, OAuth flows, or payment processing — the MCP spec prohibits collecting such information via form mode.

Checking URL Mode Support

import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Check elicitation modes")
    String checkModes(Elicitation elicitation) {
        if (elicitation.isFormModeSupported()) { (1)
            return "Form mode available";
        }
        if (elicitation.isUrlModeSupported()) { (2)
            return "URL mode available";
        }
        return "No elicitation support";
    }
}
1 Check if the client supports form mode elicitation.
2 Check if the client supports URL mode elicitation.

Basic URL Mode Request

import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.UrlElicitationRequest;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;
import java.time.Duration;

public class MyTools {

    @Tool(description = "Request API key via URL")
    Uni<String> requestApiKey(Elicitation elicitation) {
        if (!elicitation.isUrlModeSupported()) {
            return Uni.createFrom().item("URL mode not supported");
        }

        UrlElicitationRequest request = elicitation.urlRequestBuilder() (1)
            .setMessage("Please provide your API key") (2)
            .setUrl("https://myserver.example.com/provide-key") (3)
            .setCompletionTimeout(Duration.ofMinutes(15)) (4)
            .build();

        String elicitationId = request.elicitationId(); (5)

        return request.send() (6)
            .map(response -> {
                if (response.actionAccepted()) {
                    return "User consented to open the URL";
                } else {
                    return "User declined";
                }
            });
    }
}
1 Create a URL mode elicitation request builder.
2 Set the message explaining why the interaction is needed.
3 Set the URL that the user should navigate to.
4 Set the completion timeout — how long the server keeps the pending elicitation entry waiting for ElicitationCompletion.send(). Defaults to the value of quarkus.mcp.server.elicitation.default-completion-timeout (10 minutes). Expired entries are checked every minute, so the effective minimum is approximately one minute.
5 The elicitation ID is auto-generated and can be used to correlate with completion notifications.
6 The response indicates whether the user consented to navigate to the URL, not whether the interaction is complete.

Completion Notifications

After the user completes the out-of-band interaction (e.g., submits credentials on your web page), the server can notify the client by injecting ElicitationCompletion:

import io.quarkiverse.mcp.server.ElicitationCompletion;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@Path("/api-key-callback")
public class ApiKeyCallbackResource {

    @Inject
    ElicitationCompletion elicitationCompletion; (1)

    @POST
    @Path("/{elicitationId}")
    public void onApiKeyProvided(@PathParam("elicitationId") String elicitationId) {
        // Store the API key securely...

        elicitationCompletion.send(elicitationId); (2)
    }
}
1 ElicitationCompletion is a CDI bean that can be injected from any context.
2 Sends a notifications/elicitation/complete notification to the client that initiated the elicitation.

URL Elicitation Required Error

When a tool cannot proceed without URL mode elicitation, it can throw UrlElicitationRequiredException. This is automatically converted to a JSON-RPC error with code -32042:

import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.UrlElicitationRequiredException;

public class MyTools {

    @Tool(description = "Access external service")
    String accessService() {
        if (!hasStoredCredentials()) {
            var builder = UrlElicitationRequiredException.builder()
                .setMessage("Authorization required");
            String id = builder.addElicitation( (1)
                "https://myserver.example.com/authorize",
                "Please authorize access to the service");
            // Store the elicitation ID for later correlation...
            throw builder.build(); (2)
        }
        return "Service response";
    }
}
1 addElicitation() returns the auto-generated elicitation ID.
2 The exception is converted to a -32042 error with the elicitation details in the error data.

Roots

Roots allow the server to obtain the list of root directories or locations from the client. This is useful for understanding the client’s working context.

Checking Roots Support

Before using roots, check if the client supports it. This is negotiated during the MCP connection initialization.

import io.quarkiverse.mcp.server.Roots;
import io.quarkiverse.mcp.server.Tool;

public class MyTools {

    @Tool(description = "Check roots support")
    String checkRoots(Roots roots) {
        if (roots.isSupported()) { (1)
            return "Roots capability is available";
        } else {
            return "Roots not supported by this client";
        }
    }
}
1 Check if the client supports the roots capability.

Listing Roots

Request the list of roots from the client:

import io.quarkiverse.mcp.server.Root;
import io.quarkiverse.mcp.server.Roots;
import io.quarkiverse.mcp.server.Tool;
import java.util.List;

public class MyTools {

    @Tool(description = "Get client roots")
    String getRoots(Roots roots) {
        if (!roots.isSupported()) {
            return "Roots not supported";
        }

        List<Root> rootList = roots.listAndAwait(); (1)

        StringBuilder result = new StringBuilder("Client roots:\n");
        for (Root root : rootList) {
            result.append("- ").append(root.name())
                  .append(": ").append(root.uri())
                  .append("\n"); (2)
        }
        return result.toString();
    }
}
1 Request and wait for the list of roots (blocking).
2 Each root has a name and URI.

Async Roots

For non-blocking operations:

import io.quarkiverse.mcp.server.Root;
import io.quarkiverse.mcp.server.Roots;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;
import java.util.List;

public class MyTools {

    @Tool(description = "Get roots asynchronously")
    Uni<String> getRootsAsync(Roots roots) {
        if (!roots.isSupported()) {
            return Uni.createFrom().item("Not supported");
        }

        return roots.list() (1)
            .map(rootList -> {
                if (rootList.isEmpty()) {
                    return "No roots found";
                }
                return "First root: " + rootList.get(0).name();
            });
    }
}
1 Returns Uni<List<Root>> for async processing.

Responding to Root Changes

Listen for root change notifications:

import io.quarkiverse.mcp.server.McpConnection;
import io.quarkiverse.mcp.server.Notification;
import io.quarkiverse.mcp.server.Notification.Type;
import io.quarkiverse.mcp.server.Root;
import io.quarkiverse.mcp.server.Roots;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Singleton
public class RootsManager {

    private final Map<String, List<Root>> rootsCache = new ConcurrentHashMap<>();

    @Notification(Type.INITIALIZED) (1)
    void onInitialized(McpConnection connection, Roots roots) {
        if (roots.isSupported()) {
            List<Root> rootList = roots.listAndAwait();
            rootsCache.put(connection.id(), rootList); (2)
        }
    }

    @Notification(Type.ROOTS_LIST_CHANGED) (3)
    void onRootsChanged(McpConnection connection, Roots roots) {
        List<Root> updatedRoots = roots.listAndAwait();
        rootsCache.put(connection.id(), updatedRoots); (4)
    }

    public List<Root> getRoots(String connectionId) {
        return rootsCache.get(connectionId);
    }
}
1 Called when the connection is initialized.
2 Store the initial roots.
3 Called when the client’s roots change.
4 Update the cached roots.

Configuration

Configure timeouts for client integration features in application.properties:

# Sampling timeout (default: 30s)
quarkus.mcp.server.sampling.default-timeout=30s

# Elicitation response timeout (default: 60s)
quarkus.mcp.server.elicitation.default-timeout=60s

# Elicitation completion timeout (default: 10m)
# How long the server keeps pending URL mode elicitation entries
# waiting for ElicitationCompletion.send().
# Expired entries are checked every minute.
quarkus.mcp.server.elicitation.default-completion-timeout=10m

# Roots timeout (default: 10s)
quarkus.mcp.server.roots.default-timeout=10s

Error Handling

Handle timeouts and errors gracefully:

import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.SamplingMessage;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.TimeoutException;

public class MyTools {

    @Tool(description = "Sampling with error handling")
    Uni<String> robustSampling(Sampling sampling) {
        if (!sampling.isSupported()) {
            return Uni.createFrom().item("Not supported");
        }

        return sampling.requestBuilder()
            .setMaxTokens(100)
            .addMessage(SamplingMessage.withUserRole("Hello"))
            .build()
            .send()
            .map(response -> response.content().asText().text())
            .onFailure(TimeoutException.class) (1)
            .recoverWithItem("Request timed out")
            .onFailure().recoverWithItem(e -> "Error: " + e.getMessage());  (2)
    }
}
1 Handle timeout specifically.
2 Handle all other failures.