Quarkus MCP Server
"Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools."
This extension provides declarative and programmatic APIs that enable developers to implement the MCP server features easily.
| The LangChain4j project provides the MCP client functionality, either as a low-level programmatic API or as a full-fledged integration into AI-infused applications. |
Supported transports
MCP currently defines two standard transports for client-server communication.
The stdio transport starts an MCP server as a subprocess and communicates over standard in and out.
The HTTP transports connects to a running HTTP server.
The HTTP transport is defined in two variants: the "Streamable HTTP" variant (introduced in the protocol version 2025-03-26) replaces the "HTTP/SSE" transport (introduced in protocol version 2024-11-05).
The "HTTP/SSE" transport is considered deprecated but it’s still supported by most clients and servers.
This extension supports the stdio transport and both variants of the HTTP transport.
Moreover, it defines a unified API to declare server features (tools, prompts and resources).
In other words, the server features are declared using the same API but the selected transport determines the way the MCP server communicates with clients.
If you want to use the stdio transport you’ll need to add the io.quarkiverse.mcp:quarkus-mcp-server-stdio extension to your build file first.
For instance, with Maven, add the following dependency to your POM file:
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-stdio</artifactId>
<version>1.3.1</version>
</dependency>
If you use the stdio transport then your app should not write anything to the standard output. Quarkus console logging is automatically redirected to the standard error. And the standard output stream is set to "null" when the app is started by default.
|
If you want to use the HTTP transport you’ll need to add the io.quarkiverse.mcp:quarkus-mcp-server-sse extension to your build file first.
For instance, with Maven, add the following dependency to your POM file:
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-sse</artifactId>
<version>1.3.1</version>
</dependency>
This artifact contains both versions of the HTTP transport.
The MCP endpoint (as defined in 2025-03-26) is exposed at {rootPath}, i.e. /mcp by default.
The SSE endpoint (as defined in 2024-11-05) is exposed at {rootPath}/sse, i.e. /mcp/sse by default.
The {rootPath} is set to mcp by default, but it can be changed with the quarkus.mcp.server.sse.root-path configuration property.
| The Resumability and Redelivery for the Streamable HTTP is not supported yet. |
Supported server features
An MCP server provides some building blocks to enrich the context of language models in AI apps.
In this extension, a server feature (prompt, resource, tool, etc.) is either represented as a business method of a CDI bean or registered programmatically.
The execution model and context handling follow the idiomatic approach used in fundamental Quarkus extensions (such as quarkus-rest and quarkus-scheduler).
For example, when a server feature is executed, the CDI request context is active and a Vert.x duplicated context is created.
Execution model
A server feature method may use blocking or non-blocking logic.
The execution model is determined by the method signature and additional annotations such as @Blocking and @NonBlocking.
-
Methods annotated with
@RunOnVirtualThread,@Blockingor@Transactionalare considered blocking. -
Methods declared in a class annotated with
@RunOnVirtualThreadare considered blocking. -
Methods annotated with
@NonBlockingare considered non-blocking. -
Methods declared in a class annotated with
@Transactionalare considered blocking unless annotated with@NonBlocking. -
If the method does not declare any of the annotations listed above the execution model is derived from the return type:
-
Methods returning
Uniare considered non-blocking. -
Methods returning any other type are considered blocking.
-
-
Kotlin
suspendfunctions are always considered non-blocking and may not be annotated with@Blocking,@NonBlockingor@RunOnVirtualThreadand may not be in a class annotated with@RunOnVirtualThread. -
Non-blocking methods must execute on the connection’s event loop thread.
-
Blocking methods must execute on a worker thread unless annotated with
@RunOnVirtualThreador in a class annotated with@RunOnVirtualThread. -
Methods annotated with
@RunOnVirtualThreador declared in class annotated with@RunOnVirtualThreadmust execute on a virtual thread, each invocation spawns a new virtual thread.
CDI request context
Each feature method execution is associated with a new CDI request context.
This means that if a client sends a batch of MCP requests (e.g. multiple tools/call messages) then each MCP request (e.g. @Tool method invocation) receives a different instance of a @RequestScoped bean.
However, if the HTTP transport is used then all MCP requests will have the same io.vertx.core.http.HttpServerRequest injected.
Prompts
MCP provides a standardized way for servers to expose prompt templates to clients.
import io.quarkiverse.mcp.server.Prompt;
import io.quarkiverse.mcp.server.PromptArg;
import io.quarkiverse.mcp.server.PromptMessage;
import jakarta.inject.Inject;
// @Singleton (1)
public class MyPrompts {
@Inject (2)
FooService fooService;
@Prompt(description = "Put you description here.") (3)
PromptMessage foo(@PromptArg(description = "The name", defaultValue = "Max") String name) { (4)
return PromptMessage.withUserRole(new TextContent(fooService.ping(name)));
}
}
| 1 | The @Singleton scope is added automatically, if needed. |
| 2 | MyPrompts is an ordinary CDI bean. It can inject other beans, use interceptors, etc. |
| 3 | @Prompt annotates a business method of a CDI bean that should be exposed as a prompt template. By default, the name of the prompt is derived from the method name. |
| 4 | The @PromptArg annotation can be used to customize the description of an argument and set the default value that is used when a client does not provide an argument value. |
The result of a "prompt get" operation is always represented as a PromptResponse.
However, the annotated method can also return other types that are converted according to the following rules.
-
If the method returns a
PromptMessagethen the response has no description and contains the single message object. -
If the method returns a
ListofPromptMessages then the response has no description and contains the list of messages. -
If it returns any other type
XthenXis encoded using thePromptResponseEncoderAPI. -
It may also return a
Unithat wraps any of the type mentioned above.
Method parameters
A @Prompt method must only accept String parameters that represent Prompt arguments.
However, it may also accept the following parameters:
-
io.quarkiverse.mcp.server.McpConnection -
io.quarkiverse.mcp.server.McpLog -
io.quarkiverse.mcp.server.RequestId -
io.quarkiverse.mcp.server.Progress -
io.quarkiverse.mcp.server.Roots -
io.quarkiverse.mcp.server.Sampling
Completion API
Arguments of a @Prompt method may be auto-completed through the completion API.
import io.quarkiverse.mcp.server.Prompt;
import io.quarkiverse.mcp.server.PromptArg;
import io.quarkiverse.mcp.server.PromptMessage;
import jakarta.inject.Inject;
public class MyPrompts {
@Inject
FooService fooService;
@Prompt(description = "Put you description here.")
PromptMessage foo(@PromptArg(description = "The name") String name) {
return PromptMessage.withUserRole(new TextContent(fooService.ping(name)));
}
@CompletePrompt("foo") (1)
List<String> completeName(@CompleteArg(name = "name") String val) { (2) (3)
return fooService.getNames().stream().filter(n -> n.startsWith(val)).toList();
}
}
| 1 | "foo" is the name reference to a prompt. If no such prompt exists then the build fails. |
| 2 | The method returns a list of matching values. |
| 3 | The @CompleteArg annotation can be used to customize the name of an argument. |
The result of a "prompt complete" operation is always represented as a CompleteResponse.
However, the annotated method can also return other types that are converted according to the following rules.
-
If the method returns
java.lang.Stringthen the response contains a single value. -
If the method returns a
Listof `String`s then the response contains the list of values. -
The method may return a
Unithat wraps any of the type mentioned above.
A @CompletePrompt method must only accept a single String parameter that represents a completed Prompt argument.
However, it may also accept the following parameters:
-
io.quarkiverse.mcp.server.McpConnection -
io.quarkiverse.mcp.server.McpLog -
io.quarkiverse.mcp.server.RequestId -
io.quarkiverse.mcp.server.Progress -
io.quarkiverse.mcp.server.Roots -
io.quarkiverse.mcp.server.Sampling
Programmatic API
It’s also possible to register a prompt programmatically with the PromptManager API.
For example, if some prompt is only known at application startup time, it can be added as follows:
import io.quarkiverse.mcp.server.PromptManager;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;
public class MyPrompts {
@Inject
PromptManager promptManager; (1)
@Inject
CodeService codeService;
@Startup (2)
void addPrompt() {
promptManager.newPrompt("code_assist") (3)
.setDescription("Prompt for code assist")
.addArgument("lang", "Language", true)
.setHandler(
a -> PromptResponse.withMessages(
List.of(PromptMessage.withUserRole(new TextContent(codeService.assist(a.args().get("lang")))))))
.register(); (4)
}
}
| 1 | The injected manager can be used to obtain metadata and register a new prompt programmatically. |
| 2 | Ensure that addPrompt is executed when the application starts |
| 3 | The PromptManager#newPrompt(String) method returns PromptDefinition - a builder-like API. |
| 4 | Registers the prompt definition and sends the notifications/prompts/list_changed notification to all connected clients. |
Resources
MCP provides a standardized way for servers to expose resources to clients.
import io.quarkiverse.mcp.server.Resource;
import jakarta.inject.Inject;
import java.nio.file.Files;
// @Singleton (1)
public class MyResources {
@Inject (2)
FooService fooService;
@Resource(uri = "file:///project/alpha") (3)
BlobResourceContents alpha() {
return BlobResourceContents.create("file:///project/alpha", Files.readAllBytes(Paths.ALPHA));
}
}
| 1 | The @Singleton scope is added automatically, if needed. |
| 2 | MyResources is an ordinary CDI bean. It can inject other beans, use interceptors, etc. |
| 3 | @Resource annotates a business method of a CDI bean that should be exposed as a resource. By default, the name of the resource is derived from the method name. |
The result of a "resource read" operation is always represented as a ResourceResponse.
However, the annotated method can also return other types that are converted according to the following rules.
-
If the method returns an implementation of
ResourceContentsthen the response contains the single contents object. -
If the method returns a
ListofResourceContentsimplementations then the response contains the list of contents objects. -
If it returns any other type
XorList<X>thenXis encoded using theResourceContentsEncoderAPI and afterwards the rules above apply. -
It may also return a
Unithat wraps any of the type mentioned above.
| There is a default resource contents encoder registered; it encodes the returned value as JSON. |
Method parameters
A @Resource method may accept the following parameters:
-
io.quarkiverse.mcp.server.McpConnection -
io.quarkiverse.mcp.server.McpLog -
io.quarkiverse.mcp.server.RequestId -
io.quarkiverse.mcp.server.Progress -
io.quarkiverse.mcp.server.RequestUri -
io.quarkiverse.mcp.server.Roots -
io.quarkiverse.mcp.server.Sampling
Programmatic API
It’s also possible to register a resource programmatically with the ResourceManager API.
For example, if some resource is only known at application startup time, it can be added as follows:
import io.quarkiverse.mcp.server.ResourceManager;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;
public class MyResources {
@Inject
ResourceManager resourceManager; (1)
@Startup (2)
void addResource() {
resourceManager.newResource("file:///project/alpha") (3)
.setDescription("Alpha resource file")
.setHandler(
args -> new ResourceResponse(
List.of(BlobResourceContents.create(args.requestUri().value(), Files.readAllBytes(Path.of("alpha.txt"))))))
.register(); (4)
}
}
| 1 | The injected manager can be used to obtain metadata and register a new resource programmatically. |
| 2 | Ensure that addResource is executed when the application starts |
| 3 | The ResourceManager#newResource(String) method returns ResourceDefinition - a builder-like API. |
| 4 | Registers the resource definition and sends the notifications/resources/list_changed notification to all connected clients. |
Subscriptions
MCP clients can subscribe to a specific resource and receive update notifications.
import io.quarkiverse.mcp.server.ResourceManager;
import jakarta.inject.Inject;
public class MyResources {
@Inject
ResourceManager resourceManager; (1)
void alphaUpdated() {
resourceManager.getResource("file:///alpha.txt").sendUpdate(); (2)
}
}
| 1 | The injected manager can be used to obtain resource info for a specific URI. |
| 2 | Sends update notifications to all subscribers. |
Resource templates
You can also use resource templates to expose parameterized resources.
import io.quarkiverse.mcp.server.RequestUri;
import io.quarkiverse.mcp.server.ResourceTemplate;
import io.quarkiverse.mcp.server.TextResourceContents;
import jakarta.inject.Inject;
// @Singleton (1)
public class MyResourceTemplates {
@Inject (2)
ProjectService projectService;
@ResourceTemplate(uriTemplate = "file:///project/{name}") (3) (4)
TextResourceContents project(String name, RequestUri uri) { (5)
return TextResourceContents.create(uri.value(), projectService.readProject(name)));
}
}
| 1 | The @Singleton scope is added automatically, if needed. |
| 2 | MyResourceTemplates is an ordinary CDI bean. It can inject other beans, use interceptors, etc. |
| 3 | @ResourceTemplate annotates a business method of a CDI bean that should be exposed as a resource template. By default, the name of the resource template is derived from the method name. |
| 4 | ResourceTemplate#uriTemplate() contains a Level 1 URI template (RFC 6570) that can be used to construct resource URIs. |
| 5 | The name parameter refers to the expression from the URI template. The uri parameter refers to the actual resource URI. |
The result of a "resource read" operation is always represented as a ResourceResponse.
However, the annotated method can also return other types that are converted according to the following rules.
-
If the method returns an implementation of
ResourceContentsthen the response contains the single contents object. -
If the method returns a
ListofResourceContentsimplementations then the response contains the list of contents objects. -
If it returns any other type
XorList<X>thenXis encoded using theResourceContentsEncoderAPI and afterwards the rules above apply. -
It may also return a
Unithat wraps any of the type mentioned above.
A @ResourceTemplate method must only accept String parameters that represent template variables.
However, it may also accept the following parameters:
-
io.quarkiverse.mcp.server.McpConnection -
io.quarkiverse.mcp.server.McpLog -
io.quarkiverse.mcp.server.RequestId -
io.quarkiverse.mcp.server.Progress -
io.quarkiverse.mcp.server.RequestUri -
io.quarkiverse.mcp.server.Roots -
io.quarkiverse.mcp.server.Sampling
Completion API
Arguments of a @ResourceTemplate method may be auto-completed through the completion API.
import io.quarkiverse.mcp.server.ResourceTemplate;
import io.quarkiverse.mcp.server.TextResourceContents;
import jakarta.inject.Inject;
public class MyTemplates {
@Inject
ProjectService projectService;
@ResourceTemplate(uriTemplate = "file:///project/{name}")
TextResourceContents project(String name) {
return TextResourceContents.create(uri, projectService.readProject(name)));
}
@CompleteResourceTemplate("project") (1)
List<String> completeName(String name) { (2)
return projectService.getNames().stream().filter(n -> n.startsWith(name)).toList();
}
}
| 1 | "project" is the name reference to a resource template. If no such resource template exists then the build fails. |
| 2 | The method returns a list of matching values. |
The result of a "prompt complete" operation is always represented as a CompleteResponse.
However, the annotated method can also return other types that are converted according to the following rules.
-
If the method returns
java.lang.Stringthen the response contains a single value. -
If the method returns a
Listof `String`s then the response contains the list of values. -
The method may return a
Unithat wraps any of the type mentioned above.
A @CompleteResourceTemplate method must only accept a single String parameter that represents a completed Resource template variable.
However, it may also accept the following parameters:
-
io.quarkiverse.mcp.server.McpConnection -
io.quarkiverse.mcp.server.McpLog -
io.quarkiverse.mcp.server.RequestId -
io.quarkiverse.mcp.server.Progress -
io.quarkiverse.mcp.server.RequestUri -
io.quarkiverse.mcp.server.Roots -
io.quarkiverse.mcp.server.Sampling
Programmatic API
It’s also possible to register a resource template programmatically with the ResourceTemplateManager API.
For example, if some resource template is only known at application startup time, it can be added as follows:
import io.quarkiverse.mcp.server.ResourceTemplateManager;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;
public class MyResourceTemplates {
@Inject
ResourceTemplateManager resourceTemplateManager; (1)
@Inject
FileService fileService;
@Startup (2)
void addResourceTemplate() {
resourceTemplateManager.newResourceTemplate("alpha") (3)
.setUriTemplate("file:///alpha/{foo}")
.setDescription("Alpha file template")
.setHandler(
rta -> new ResourceResponse(
List.of(BlobResourceContents.create(args.requestUri().value(), fileService.load(rta.args().get("foo"))))))
.register(); (4)
}
}
| 1 | The injected manager can be used to obtain metadata and register a new resource template programmatically. |
| 2 | Ensure that addResourceTemplate is executed when the application starts |
| 3 | The ResourceTemplateManager#newResourceTemplate(String) method returns ResourceTemplateDefinition - a builder-like API. |
| 4 | Registers the resource template definition. |
Tools
MCP provides a standardized way for servers to expose tools that can be invoked by clients.
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolResponse;
import jakarta.inject.Inject;
// @Singleton (1)
public class MyTools {
@Inject (2)
FooService fooService;
@Tool(description = "Put you description here.") (3)
ToolResponse foo(@ToolArg(description = "The name", defaultValue = "Andy") String name) { (4)
return ToolResponse.success(
new TextContent(fooService.ping(name)));
}
}
| 1 | The @Singleton scope is added automatically, if needed. |
| 2 | MyTools is an ordinary CDI bean. It can inject other beans, use interceptors, etc. |
| 3 | @Tool annotates a business method of a CDI bean that should be exposed as a tool. By default, the name of the tool is derived from the method name. |
| 4 | The @ToolArg annotation can be used to customize the description of an argument and set the default value that is used when a client does not provide an argument value. |
If a method annotated with @Tool throws a io.quarkiverse.mcp.server.ToolCallException then the response is a failed ToolResponse and the message of the exception is used as the text of the result content. It is also possible to annotate the method or declaring class with @io.quarkiverse.mcp.server.WrapBusinessError - in that case, an exception thrown is wrapped automatically in a ToolCallException if it’s assignable from any of the specified exception classes.
|
A result of a "tool call" operation is always represented as a ToolResponse.
However, the annotated method can also return other types that are converted according to the following rules.
-
If the method returns
java.lang.Stringthen the response is "success" and contains the singleTextContentobject. -
If the method returns an implementation of
io.quarkiverse.mcp.server.Contentthen the response is "success" and contains the single content object. -
If the method returns a
ListofContentimplementations or `String`s then the response is "success" and contains the list of relevant content objects. -
The method may return a
Unithat wraps any of the type mentioned above. -
If it returns
java.lang.Stringthen the response is "success" and contains a singleTextContent. -
If it returns an implementation of
Contentthen the response is "success" and contains a single content object. -
If it returns a
ListofContentimplementations or strings then the response is "success" and contains a list of relevant content objects. -
If it returns any other type
XorList<X>thenXis encoded using theToolResponseEncoderandContentEncoderAPI and afterwards the rules above apply. -
It may also return a
Unithat wraps any of the type mentioned above.
| There is a default content encoder registered; it encodes the returned value as JSON. |
Method parameters
A @Tool method may accept parameters that represent Tool arguments.
However, it may also accept the following parameters:
-
io.quarkiverse.mcp.server.McpConnection -
io.quarkiverse.mcp.server.McpLog -
io.quarkiverse.mcp.server.RequestId -
io.quarkiverse.mcp.server.Progress -
io.quarkiverse.mcp.server.Roots -
io.quarkiverse.mcp.server.Sampling
Programmatic API
It’s also possible to register a tool programmatically with the ToolManager API.
For example, if some tool is only known at application startup time, it can be added as follows:
import io.quarkiverse.mcp.server.ToolManager;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;
public class MyTools {
@Inject
ToolManager toolManager; (1)
@Startup (2)
void addTool() {
toolManager.newTool("toLowerCase") (3)
.setDescription("Converts input string to lower case.")
.addArgument("value", "Value to convert", true, String.class)
.setHandler(
ta -> ToolResponse.success(ta.args().get("value").toString().toLowerCase()))
.register(); (4)
}
}
| 1 | The injected manager can be used to obtain metadata and register a new tool programmatically. |
| 2 | Ensure that addTool is executed when the application starts |
| 3 | The ToolManager#newTool(String) method returns ToolDefinition - a builder-like API. |
| 4 | Registers the tool definition and sends the notifications/tools/list_changed notification to all connected clients. |
Support @Tool/@P annotations from LangChain4j
The @dev.langchain4j.agent.tool.Tool and @dev.langchain4j.agent.tool.P annotations from LangChain4j can be used instead of @Tool/@ToolArg.
However, keep in mind that semantics may vary and follows the rules defined in this documentation.
For example, void methods are not supported.
Customizing JSON Schema Generation
The MCP server uses the com.github.victools:jsonschema-generator library to generate JSON schemas for tool inputs.
This library is configurable through modules that process various annotations (e.g., Jackson, Bean Validation) to enrich the generated schemas.
By defining a dependency on com.github.victools:jsonschema-module-jackson, the schema generator server will be automatically configured to use the Jackson module.
The same goes for com.github.victools:jsonschema-module-jakarta-validation and `com.github.victools:jsonschema-module-swagger-2.
See the Extension configuration reference for relevant config properties.
Notifications
You can annotate a business method of a CDI bean with @io.quarkiverse.mcp.server.Notification.
This method will be called when an MCP client sends a specific notification message, such as notifications/initialized.
import io.quarkiverse.mcp.server.Notification;
import io.quarkiverse.mcp.server.Notification.Type;
public class MyNotifications {
@Notification(Type.INITIALIZED) (1)
void init(McpConnection connection) { (2) (3)
Log.infof("New client connected: %s", connection.initialRequest().implementation().name());
}
}
| 1 | Invoke the method when a client sends the notifications/initialized message. |
| 2 | The annotated method must either return void or Uni<Void>. |
| 3 | The method may accept the following parameters: McpConnection, McpLog and Roots. |
Programmatic API
It’s also possible to register a notification programmatically with the NotificationManager API.
import io.quarkiverse.mcp.server.NotificationManager;
import io.quarkiverse.mcp.server.Notification.Type;
import jakarta.inject.Inject;
import io.quarkus.runtime.Startup;
public class MyNotifications {
@Inject
NotificationManager notificationManager; (1)
@Startup (2)
void addNotification() {
notificationManager.newNotification("foo")
.setType(Type.INITIALIZED)
.setHandler(args -> {
Log.infof("New client connected: %s", args.connection().initialRequest().implementation().name());
return null;
}).register(); (3)
}
}
| 1 | The injected manager can be used to register a new notification programmatically. |
| 2 | Instructs Quarkus to execute the addNotification() method when the application starts. |
| 3 | Registers the notification definition. |
Pagination
Pagination is automatically enabled if the number of results exceeds the configured page size.
See the Extension configuration reference for relevant config properties.
The following MCP operations support pagination: resources/list, resources/templates/list, prompts/list and tools/list.
Client logging
Methods annotated with @Tool, @Resource, @ResourceTemplate, @Prompt and @CompletePrompt may accept a parameter of type io.quarkiverse.mcp.server.McpLog.
McpLog is a utility class that can send log message notifications to a connected MCP client.
There are also convenient methods that log the message first (using JBoss Logging) and afterwards send a notification message with the same content.
package org.acme;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolResponse;
import jakarta.inject.Inject;
public class MyTools {
@Tool
String up(String name, McpLog log) {
log.info("UP name accepted %s", name); (1)
return name.toUpperCase();
}
@Tool
String down(String name, McpLog log) {
log.send(LogLevel.INFO, "DOWN name accepted %s", name); (2)
return name.toLoweCase();
}
}
| 1 | If the tool method is called with argument name=Lu then (A) an equivalent of org.jboss.logging.Logger.getLogger("org.acme.MyTools").infof("UP name accepted %s", name) is called, and (B) subsequently, a notification with parameters {"level":"info","logger":"tool:up","data":"UP name accepted: Lu"} is sent to the connected client. |
| 2 | If the tool method is called with argument name=Foo then a log message notification with parameters {"level":"info","logger":"tool:down","data":"DOWN name accepted: Foo"} is sent to the connected client. |
The default log level can be set with the quarkus.mcp.server.client-logging.default-level configuration property.
|
Progress API
Server features which have the nature of long running operations can send progress notifications to the clients.
A server feature method can accept the io.quarkiverse.mcp.server.Progress parameter.
This API makes it possible to check the progress token from a client request.
Furthermore, you can either send the notifications directly using the Progress#notificationBuilder() method, or build a stateful thread-safe ProgressTracker object that can be be used to update the progress status and send notification messages in one step.
ProgressTracker Exampleimport java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import jakarta.annotation.PreDestroy;
import io.smallrye.mutiny.Uni;
import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.ProgressTracker;
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
private final ExecutorService executor;
MyTools() {
this.executor = Executors.newFixedThreadPool(1);
}
@PreDestroy
void destroy() {
executor.shutdownNow();
}
@Tool
Uni<String> longRunning(Progress progress) { (1) (2)
if (progress.token().isEmpty()) { (3)
return Uni.createFrom().item("nok");
}
ProgressTracker tracker = progress.trackerBuilder() (4)
.setDefaultStep(1)
.setTotal(10.2)
.setMessageBuilder(i -> "Long running progress: " + i)
.build();
CompletableFuture<String> ret = new CompletableFuture<String>();
executor.execute(() -> {
for (int i = 0; i < 10; i++) {
try {
// Do something that takes time...
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
tracker.advance(); (5)
}
ret.complete("ok");
});
return Uni.createFrom().completionStage(ret);
}
}
| 1 | A server feature method can accept the io.quarkiverse.mcp.server.Progress parameter. |
| 2 | Long running operations must return Uni so that the server can process them asynchronously. |
| 3 | The server should only send notifications if the client request contains the progress token. |
| 4 | ProgressTracker is a stateful thread-safe object can be be used to update the progress status and send notification messages to the client. |
| 5 | Advance the progress and send a notifications/progress message to the client without waiting for the result. |
ProgressNotification Exampleimport java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import jakarta.annotation.PreDestroy;
import io.smallrye.mutiny.Uni;
import io.quarkiverse.mcp.server.Progress;
import io.quarkiverse.mcp.server.ProgressNotification;
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
private final ExecutorService executor;
MyTools() {
this.executor = Executors.newFixedThreadPool(1);
}
@PreDestroy
void destroy() {
executor.shutdownNow();
}
@Tool
Uni<String> longRunning(Progress progress) { (1) (2)
if (progress.token().isEmpty()) { (3)
return Uni.createFrom().item("nok");
}
CompletableFuture<String> ret = new CompletableFuture<String>();
executor.execute(() -> {
for (int i = 0; i < 10; i++) {
try {
// Do something that takes time...
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
progress.notificationBuilder()
.setProgress(i)
.setTotal(10.2)
.setMessage("Long running progress: " + i)
.build()
.sendAndForget(); (4)
}
ret.complete("ok");
});
return Uni.createFrom().completionStage(ret);
}
}
| 1 | A server feature method can accept the io.quarkiverse.mcp.server.Progress parameter. |
| 2 | Long running operations must return Uni so that the server can process them asynchronously. |
| 3 | The server should only send notifications if the client request contains the progress token. |
| 4 | Send the message to the client without waiting for the result. |
Roots
If an MCP client supports the roots capability the server can obtain the list of root objects.
Any server feature method can accept the io.quarkiverse.mcp.server.Roots parameter.
import io.quarkiverse.mcp.server.Roots;
import io.quarkiverse.mcp.server.Notification;
import io.quarkiverse.mcp.server.Notification.Type;
public class MyRoots {
private final Map<String, List<Root>> rootsMap = new ConcurrentHashMap<>(); (1)
@Notification(Type.INITIALIZED)
void init(McpConnection connection, Roots roots) { (2)
if (connection.initialRequest().supportsRoots()) {
rootsMap.put(connection.id(), roots.listAndAwait());
}
}
@Notification(Type.ROOTS_LIST_CHANGED)
void change(McpConnection connection, Roots roots) { (3)
rootsMap.put(connection.id(), roots.listAndAwait());
}
public List<Root> getRoots(String connectionId) {
return rootsMap.get(connectionId);
}
}
| 1 | Maps connection ids (client sessions) to lists of roots. |
| 2 | Obtain the list of roots when an MCP client sends the notifications/initialized message. |
| 3 | Update the list of roots when an MCP client sends the notifications/roots/list_changed message. |
Sampling
If an MCP client supports the sampling capability the server can request LLM sampling from language models via client.
Any server feature method can accept the io.quarkiverse.mcp.server.Sampling parameter.
import io.quarkiverse.mcp.server.Sampling;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;
public class MyTools {
@Tool(description = "A tool that is using sampling...")
Uni<String> samplingFoo(Sampling sampling) { (1)
if (sampling.isSupported()) {
SamplingRequest samplingRequest = sampling.requestBuilder() (2)
.setMaxTokens(100)
.addMessage(SamplingMessage.withUserRole("What's happening?"))
.build();
return samplingRequest.send().map(resp -> resp.content().asText().text()); (3)
} else {
return Uni.createFrom().item("Sampling not supported");
}
}
}
| 1 | The Sampling parameter is injected automatically. |
| 2 | If sampling is supported a convenient builder can be used to construct a SamplingRequest. |
| 3 | The server sends a sampling request and when a sampling response returns the tool method completes. |
Security
In case of using the HTTP/SSE transport, you can secure the MCP SSE endpoints using the Quarkus web security layer.
application.propertiesquarkus.http.auth.permission.mcp-endpoints.paths=/mcp/sse,/mcp/messages/* (1)
quarkus.http.auth.permission.mcp-endpoints.policy=authenticated (2)
| 1 | Apply the mcp-endpoints policy to the default MCP SSE endpoint - /mcp/sse, and also to all MCP message endpoints - /mcp/messages/*. |
| 2 | Permit only authenticated users. |
Alternatively, you can also secure the annotated server feature methods with security annotations such as io.quarkus.security.Authenticated,
jakarta.annotation.security.RolesAllowed and other annotations listed in the Supported security annotations documentation.
However, in this case an MCP client will not receive an appropriate HTTP status code if authentication fails.
Instead, an MCP error message with code -32001 is sent back to the client.
package org.acme;
import jakarta.annotation.security.RolesAllowed;
import io.quarkus.security.Authenticated;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolResponse;
import jakarta.inject.Inject;
@Authenticated (1)
public class MyTools {
@Tool
String up(String name, McpLog log) {
log.info("UP name accepted %s", name); (1)
return name.toUpperCase();
}
@Tool
@RolesAllowed("admin") (2)
String down(String name, McpLog log) {
log.send(LogLevel.INFO, "DOWN name accepted %s", name); (2)
return name.toLoweCase();
}
}
| 1 | Permit only authenticated users. All CDI business methods are protected. |
| 2 | Permit only user with role admin. |
Initial checks
The CDI beans that implement io.quarkiverse.mcp.server.InitialCheck are used to perform an initial check when an MCP client connection is initialized, i.e. before the server capabilities are sent back to the client.
If an initial check fails then the connection is not initialized successfully and the error message is sent back to the client.
Multiple checks are sorted by InjectableBean#getPriority() and executed sequentially.
Higher priority is executed first.
InitialCheck Examplepackage org.acme;
import io.quarkiverse.mcp.server.InitialCheck;
import io.quarkiverse.mcp.server.InitialRequest;
// @Singleton is added automatically
public class MyCheck implements InitialCheck {
public Uni<CheckResult> perform(InitialRequest initialRequest) {
return initialRequest.supportsSampling() ? InitialCheck.CheckResult.successs()
: InitialCheck.CheckResult.error("Sampling not supported");
}
}
Filters
It is possible to determine the set of visible/accesible tools, prompts, resources and resource templates for a specific MCP client.
Any CDI bean that implements ToolFilter, PrompFilter, ResourceFilter and ResourceTemplateFilter respectively is automatically applied.
Filters should be fast and efficient, and should never block the current thread (read data from a socket, write data to disk, etc.) because they can be executed on an event loop.
If a filter throws an unchecked exception then its execution is ignored and the next filter is applied.
Multiple filters are sorted by InjectableBean#getPriority() and executed sequentially.
Higher priority is executed first.
Only features that match all the filters are visible/accesible.
package org.acme;
import io.quarkiverse.mcp.server.ToolFilter;
import io.quarkiverse.mcp.server.PromptFilter;
// @Singleton is added automatically
public class MyFilters implements ToolFilter, PromptFilter {
@Override
public boolean test(PromptInfo prompt, McpConnection connection) {
// Skip clients that do not support sampling
return connection.initialRequest().supportsSampling();
}
@Override
public boolean test(ToolInfo tool, McpConnection connection) {
// Skip tools registered programmatically
return tool.isMethod();
}
}
Multiple server configurations
Multiple server configurations only make sense for transports that support multiple MCP clients (e.g. HTTP/SSE). Therefore, the application startup will fail if multiple server configurations are detected and the stdio transport is used.
|
It is possible to bind features, such as tools, prompts, and resources, to a specific server configuration.
Typically, you might need to define multiple MCP endpoints to handle different security requirements.
Another option is that it’s possible to change the name of the server included in the response to an initialize request.
A feature is bound to exactly one server configuration. The default configuration is used unless an explicit binding exists.
quarkus.mcp.server.server-info.name=Alpha server (1)
quarkus.mcp.server.bravo.server-info.name=Bravo server (2)
quarkus.mcp.server.sse.root-path=/alpha/mcp (3)
quarkus.mcp.server.bravo.sse.root-path=/bravo/mcp (4)
| 1 | Set the server name for the default server. Note that the default server name can be omitted. |
| 2 | Set the server name for the bravo server. |
| 3 | Set the root path for the default server, i.e. the MCP endpoint is exposed at /alpha/mcp. |
| 4 | Set the root path for the bravo server, i.e. the MCP endpoint is exposed at /bravo/mcp. |
You can use the @io.quarkiverse.mcp.server.McpServer annotation to bind a feature to a server configuration declaratively.
If no @McpServer annotation is declared then the default server configuration is used.
@McpServer Examplepackage org.acme;
import io.quarkiverse.mcp.server.McpServer;
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
// No @McpServer annotation means the default server configuration
@Tool(description = "Put you description here.")
String atool() {
return "...some content";
}
@McpServer("bravo") (1)
@Tool(description = "Put you description here.")
String btool() {
return "...some content";
}
}
| 1 | MyTools#btool is bound to the server configuration bravo, i.e. it’s exposed with the /bravo/mcp endpoint. It’s not available at /alpha/mcp. |
The programmatic API also makes it possible to set the server configuration name; e.g. ToolDefinition#setServerName().
|
Traffic logging
The extension can log JSON messages sent and received for debugging purposes.
To enable traffic logging, set the quarkus.mcp.server.traffic-logging.enabled configuration property to true.
Note that the number of logged characters is limited.
The default limit is 100, but you can change this limit with the quarkus.mcp.server.traffic-logging.text-limit configuration property.
quarkus.mcp.server.traffic-logging.enabled=true (1)
quarkus.mcp.server.traffic-logging.text-limit=500 (2)
| 1 | Enables traffic logging. |
| 2 | Set the number of characters of a JSON message which will be logged. |
Dev UI
The quarkus-mcp-server-sse extension provides convenient Dev UI views for tools, prompts, resources and resource templates.
You can inspect and test the server features easily without third-party tools like MCP Inspector
MCP Inspector
The MCP Inspector is a developer tool for testing and debugging MCP servers. It’s a Node.js app that can be run locally:
$ npx @modelcontextprotocol/inspector
The UI is then available at locahost:5173 by default.
If you don’t have the npx command installed locally you can also use the official Node.js Docker image.
Linux developers will need to add the --network=host option because the inspector app needs to access your MCP server running on the localhost:
$ docker run --rm --network=host node:18 npx @modelcontextprotocol/inspector
However, --network=host does not work for Mac and Windows.
Mac and Windows developers will need to export the default MCP inspector port (5137) and use the host.docker.internal special DNS name instead of localhost in the Server connection pane UI.
When inspecting a server that uses the "Streamable HTTP" or HTTP/SSE transport, you need to configure the proper path in the MCP inspector. The path should be ${quarkus.mcp.server.sse.root-path} for "Streamable HTTP" and ${quarkus.mcp.server.sse.root-path}/sse for HTTP/SSE; i.e. /mcp and /mcp/sse by default.
|
$ docker run --rm -p 5173:5173 node:18 npx @modelcontextprotocol/inspector
Extension configuration reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
The strategy used when server features, such as tools, prompts, and resources, reference an non-existent server name. Environment variable: |
|
|
Whether to use the SchemaGenerator’s Jackson Module. If this module is not present as a dependency, this module won’t be enabled. Environment variable: |
boolean |
|
Corresponds to If enabled, the order of properties in the generated schema will respect the order defined in a Environment variable: |
boolean |
|
Corresponds to If enabled, a property will be marked as "required" in the schema if its corresponding field or method is annotated with Environment variable: |
boolean |
|
Corresponds to If enabled, the schema for an enum will be a simple array of values (e.g., strings) derived from the method annotated with Environment variable: |
boolean |
|
Corresponds to If enabled, the schema for an enum will be derived from Environment variable: |
boolean |
|
Corresponds to If enabled, only methods explicitly annotated with Environment variable: |
boolean |
|
Corresponds to If enabled, any configured Environment variable: |
boolean |
|
Corresponds to If enabled, subtypes in a polymorphic hierarchy will always be represented by a Environment variable: |
boolean |
|
Corresponds to A specialized option for handling subtypes that have been transformed. Environment variable: |
boolean |
|
Corresponds to If enabled, subtype resolution via Environment variable: |
boolean |
|
Corresponds to If enabled, the transformation of the schema based on a Environment variable: |
boolean |
|
Corresponds to If enabled, properties referencing an object that has an ID (via Environment variable: |
boolean |
|
Whether to use the SchemaGenerator’s Swagger 2 Module. If this module is not present as a dependency, this module won’t be enabled. Environment variable: |
boolean |
|
Whether to use the SchemaGenerator’s Jakarta Validation Module. If this module is not present as a dependency, this module won’t be enabled. Environment variable: |
boolean |
|
Corresponds to If enabled, a field annotated with a "not-nullable" constraint (e.g., Environment variable: |
boolean |
|
Corresponds to If enabled, a method (typically a getter) annotated with a "not-nullable" constraint (e.g., Environment variable: |
boolean |
|
Corresponds to If enabled, for properties annotated with Environment variable: |
boolean |
|
Corresponds to If enabled, for properties annotated with Environment variable: |
boolean |
|
The name of the server is included in the response to an Environment variable: |
string |
|
The version of the server is included in the response to an Environment variable: |
string |
|
If set to Environment variable: |
boolean |
|
The number of characters of a text message which will be logged if traffic logging is enabled. Environment variable: |
int |
|
The default log level. Environment variable: |
|
|
The interval after which, when set, the server sends a ping message to the connected client automatically. Ping messages are not sent automatically by default. Environment variable: |
||
If the number of resources exceeds the page size then pagination is enabled and the given page size is used. Environment variable: |
int |
|
If the number of resource templates exceeds the page size then pagination is enabled and the given page size is used. Environment variable: |
int |
|
If the number of tools exceeds the page size then pagination is enabled and the given page size is used. Environment variable: |
int |
|
If the number of prompts exceeds the page size then pagination is enabled and the given page size is used. Environment variable: |
int |
|
The default timeout for a sampling request. Negative and zero durations imply no timeout. Environment variable: |
|
|
The default timeout to list roots. Negative and zero durations imply no timeout. Environment variable: |
|
|
If set to Environment variable: |
boolean |
|
The amount of time that a connection can be inactive. The connection might be automatically closed when the timeout expires. Negative and zero durations imply no timeout. The Environment variable: |
|
|
About the Duration format
To write duration values, use the standard You can also use a simplified format, starting with a number:
In other cases, the simplified format is translated to the
|
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
Flag to specify whether the MCP server should be automatically initialized. This can be useful in case where the MCP server should be conditionally started. For example: from a CLI that provides multiple commands including one for starting the MCP server. Environment variable: |
boolean |
|
If set to Keep in mind that console logging is still automatically redirected to the standard error. You will need to set the
Environment variable: |
boolean |
|
If set to Environment variable: |
boolean |
|
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
The MCP endpoint (as defined in the specification The SSE endpoint (as defined in the specification Environment variable: |
string |
|
If set to true then the query params from the initial HTTP request should be included in the message endpoint. Environment variable: |
boolean |
|