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
The MCP specification 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 also support the unofficial websocket transport.
It also 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.7.0</version>
</dependency>| If you use the stdiotransport 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.7.0</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. | 
If you want to use the WebSocket transport you’ll need to add the io.quarkiverse.mcp:quarkus-mcp-server-websocket 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-websocket</artifactId>
    <version>1.7.0</version>
</dependency>The WebSocket endpoint is exposed at {endpointPath}.
The {endpointPath} is set to /mcp/ws by default, but it can be changed with the quarkus.mcp.server.websocket.endpoint-path configuration property.
| You can also use the BOM (Bill Of Materials) to control the versions of all   | 
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.
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 @Singletonscope is added automatically, if needed. | 
| 2 | MyToolsis an ordinary CDI bean. It can inject other beans, use interceptors, etc. | 
| 3 | @Toolannotates 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 @ToolArgannotation 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 @Toolthrows aio.quarkiverse.mcp.server.ToolCallExceptionthen the response is a failedToolResponseand 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 aToolCallExceptionif 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
- 
io.quarkiverse.mcp.server.Elicitation
- 
io.quarkiverse.mcp.server.Cancellation
- 
io.quarkiverse.mcp.server.RawMessage
| If you need to validate the parameters of a @Toolmethod then the Hibernate Validator integration fits perfectly. | 
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 addToolis executed when the application starts | 
| 3 | The ToolManager#newTool(String)method returnsToolDefinition- a builder-like API. | 
| 4 | Registers the tool definition and sends the notifications/tools/list_changednotification 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.
| The default behavior can be changed with quarkus.mcp.server.support-langchain4j-annotations=false. | 
Customizing JSON Schema Generation
By default, 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.
However, it is also possible to override the default behavior.
First, you can customize the input schema generation on the method level, using a custom io.quarkiverse.mcp.server.InputSchemaGenerator together with Tool.InputSchema#generator().
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import jakarta.inject.Inject;
public class MyTools {
    @Tool(description = "Put you description here.", inputSchema = @InputSchema(generator = MySchemaGenerator.class)) (1)
    String foo(@ToolArg(description = "The name", defaultValue = "Lina") String name) {
        return "Foo name is " + name;
    }
}| 1 | The MySchemaGeneratoris used to generate the input schema for this@Toolmethod.InputSchemaGeneratorimplementations must be CDI beans. Qualifiers are ignored. | 
Furthermore, you can also implement a custom io.quarkiverse.mcp.server.GlobalInputSchemaGenerator.
This generator is then used for all @Tool methods instead of the buil-in implementation.
Caching Generated JSON Schemas
If your application contains a lot of tools with complex input/output schemas it might make sense to cache the generated schemas so that they are not re-generated for each tools/list request.
You can leverage CDI decorators to implement a simple cache:
import io.quarkiverse.mcp.server.GlobalInputSchemaGenerator;
import jakarta.inject.Inject;
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.annotation.Priority;
@Priority(1) (1)
@Decorator (2)
public class CachingGlobalSchemaGeneratorDecorator implements GlobalInputSchemaGenerator {
   private final ConcurrentMap<String, InputSchema> cache = new ConcurrentHashMap<>();
   @Inject
   @Delegate
   GlobalInputSchemaGenerator delegate; (3)
   @Override
   public InputSchema generate(ToolInfo tool) {
      return cache.computeIfAbsent(tool.name(), k -> {
            return delegate.generate(tool); (4)
      });
   }
}| 1 | @Priorityenables the decorator. Decorators with smaller priority values are called first. | 
| 2 | @Decoratormarks a decorator component. | 
| 3 | Each decorator must declare exactly one delegate injection point. The decorator applies to beans that are assignable to this delegate injection point. | 
| 4 | The decorator may invoke any method of the delegate object. And the container invokes either the next decorator in the chain or the business method of the intercepted instance. | 
| CDI decorators are similar to CDI interceptors, but because they implement interfaces with business semantics, they are able to implement business logic. | 
Structured Content
Since MCP 2025-06-18 it’s possible to return a structured content in a tool response.
Where structured content means basically a custom JSON object.
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import jakarta.inject.Inject;
public class MyTools {
    @Inject
    FooService fooService;
    @Tool(description = "Put you description here.", structuredContent = true) (1) (2)
    Foo foo(@ToolArg(description = "The name", defaultValue = "Lina") String name) {
        return fooService.ping(name);
    }
}| 1 | If Tool#structuredContent()is totrueand the method returns a typeXwhich is not specifically treated (see the conversion rules), then the return value is converted to JSON and used as thestructuredContentof the result. | 
| 2 | Also the output schema is generated automatically from the return type. | 
If a tool method returns the ToolResponse directly then the return type may not be used for schema generation.
In this case, the Tool#outputSchema() can be used to define the output schema for validation of results with structured content.
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Tool.OutputSchema;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolResponse;
import jakarta.inject.Inject;
public class MyTools {
    @Inject
    FooService fooService;
    @Tool(description = "Put you description here.", outputSchema = @OutputSchema(from = Foo.class)) (1)
    ToolResponse foo(@ToolArg(description = "The name", defaultValue = "Lina") String name) {
        return ToolResponse.structuredSuccess(fooService.ping(name));
    }
}| 1 | The Tool#outputSchema()is used to define the class from which the output schema is generated. | 
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 @Singletonscope is added automatically, if needed. | 
| 2 | MyPromptsis an ordinary CDI bean. It can inject other beans, use interceptors, etc. | 
| 3 | @Promptannotates 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 @PromptArgannotation 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
- 
io.quarkiverse.mcp.server.Elicitation
- 
io.quarkiverse.mcp.server.Cancellation
- 
io.quarkiverse.mcp.server.RawMessage
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 @CompleteArgannotation 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
- 
io.quarkiverse.mcp.server.Elicitation
- 
io.quarkiverse.mcp.server.Cancellation
- 
io.quarkiverse.mcp.server.RawMessage
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 addPromptis executed when the application starts | 
| 3 | The PromptManager#newPrompt(String)method returnsPromptDefinition- a builder-like API. | 
| 4 | Registers the prompt definition and sends the notifications/prompts/list_changednotification 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 @Singletonscope is added automatically, if needed. | 
| 2 | MyResourcesis an ordinary CDI bean. It can inject other beans, use interceptors, etc. | 
| 3 | @Resourceannotates 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
- 
io.quarkiverse.mcp.server.Elicitation
- 
io.quarkiverse.mcp.server.Cancellation
- 
io.quarkiverse.mcp.server.RawMessage
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 addResourceis executed when the application starts | 
| 3 | The ResourceManager#newResource(String)method returnsResourceDefinition- a builder-like API. | 
| 4 | Registers the resource definition and sends the notifications/resources/list_changednotification 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 @Singletonscope is added automatically, if needed. | 
| 2 | MyResourceTemplatesis an ordinary CDI bean. It can inject other beans, use interceptors, etc. | 
| 3 | @ResourceTemplateannotates 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 nameparameter refers to the expression from the URI template. Theuriparameter 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
- 
io.quarkiverse.mcp.server.Elicitation
- 
io.quarkiverse.mcp.server.Cancellation
- 
io.quarkiverse.mcp.server.RawMessage
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
- 
io.quarkiverse.mcp.server.Elicitation
- 
io.quarkiverse.mcp.server.Cancellation
- 
io.quarkiverse.mcp.server.RawMessage
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 addResourceTemplateis executed when the application starts | 
| 3 | The ResourceTemplateManager#newResourceTemplate(String)method returnsResourceTemplateDefinition- a builder-like API. | 
| 4 | Registers the resource template definition. | 
| The MCP specification of Structured Content mentions: For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block. 
— MCP specification 2025-06-18
 While the specification says SHOULD, some MCP clients actually expect the JSON  | 
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/initializedmessage. | 
| 2 | The annotated method must either return voidorUni<Void>. | 
| 3 | The method may accept the following parameters: McpConnection,McpLogandRoots. | 
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. | 
Advanced APIs
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=Luthen (A) an equivalent oforg.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=Foothen 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-levelconfiguration 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.Progressparameter. | 
| 2 | Long running operations must return Uniso that the server can process them asynchronously. | 
| 3 | The server should only send notifications if the client request contains the progress token. | 
| 4 | ProgressTrackeris 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/progressmessage 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.Progressparameter. | 
| 2 | Long running operations must return Uniso 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/initializedmessage. | 
| 3 | Update the list of roots when an MCP client sends the notifications/roots/list_changedmessage. | 
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 Samplingparameter 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. | 
Elicitation
If an MCP client supports the elicitation capability, then the server can request additional information from the client.
Any server feature method can accept the io.quarkiverse.mcp.server.Elicitation parameter to build an elicitation request.
import io.quarkiverse.mcp.server.Elicitation;
import io.quarkiverse.mcp.server.ElicitationRequest;
import io.quarkiverse.mcp.server.ElicitationResponse;
import io.quarkiverse.mcp.server.Tool;
import io.smallrye.mutiny.Uni;
public class MyTools {
   @Tool(description = "A tool that is using elicitation...")
   Uni<String> elicitationFoo(Elicitation elicitation) { (1)
      if (elicitation.isSupported()) {
         ElicitationRequest request = elicitation.requestBuilder() (2)
                .setMessage("What's your GitHub account?")
                .addSchemaProperty("username", new StringSchema())
                .build();
         return request.send().map(response -> { (3)
                    if (response.actionAccepted()) {
                        return "It's GitHub user: " + response.content().getString("username") + ".";
                    } else {
                        return "Not accepted...";
                    }
         });
     } else {
        return Uni.createFrom().item("Elicitation not supported.);
     }
  }
}| 1 | The Elicitationparameter is injected automatically. | 
| 2 | If elicitation is supported then a convenient builder can be used to construct a ElicitationRequest. | 
| 3 | The server sends an elicitation request and when an elicitation response returns the tool method completes. | 
Cancellation
MCP supports optional cancellation of in-progress requests.
The io.quarkiverse.mcp.server.Cancellation interface can be used to determine if an MCP client requested a cancellation of the current request.
Feature methods can accept this class as a parameter.
It will be automatically injected before the method is invoked.
import io.quarkiverse.mcp.server.Cancellation;
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
   @Tool(description = "A tool that may be cancelled")
   String myTool(Cancellation cancellation) { (1)
       while (someCondition) {
          if (cancellation.check().isRequested()) { (2) (3)
            throw new OperationCancellationException();
          }
          // do something...
          TimeUnit.MILLISECONDS.sleep(500);
       }
       return "OK";
   }
}| 1 | The Cancellationparameter is injected automatically. | 
| 2 | Perform the check and if cancellation is requested then skip the processing, i.e. throw an OperationCancellationException. | 
| 3 | The convenient method Cancellation#skipProcessingIfCancelled()can be used instead. | 
Raw messages
Any server feature method can accept the io.quarkiverse.mcp.server.RawMessage parameter.
A raw message represents an unprocessed MCP request or notification from an MCP client.
import io.quarkiverse.mcp.server.RawMessage;
import io.quarkiverse.mcp.server.Tool;
public class MyTools {
   @Tool(description = "A tool that can access the raw message")
   String myTool(RawMessage message, int count) { (1)
       // A raw message may look like:
       // {
       //  "jsonrpc" : "2.0",
       //  "method" : "tools/call",
       //  "id" : 2,
       //  "params" : {
       //    "name" : "foo",
       //    "arguments" : {
       //      "count" : 2
       //    }
       //   }
       // }
       // rawCount == count
       int rawCount = message.asJsonObject()
                    .getJsonObject("params")
                    .getJsonObject("arguments")
                    .getInteger("count"); (2)
       return "OK".repeat(rawCount);
   }
}| 1 | The RawMessageparameter is injected automatically. | 
| 2 | We can use the JsonObjectAPI to inspect the raw message from the client. | 
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();
   }
}Security
In case of using the HTTP transport, you can secure MCP Streamable HTTP and SSE endpoints using the Quarkus web security layer.
application.propertiesquarkus.http.auth.permission.mcp-endpoints.paths=/mcp/* (1)
quarkus.http.auth.permission.mcp-endpoints.policy=authenticated (2)| 1 | Apply the mcp-endpointspolicy to all requests targeting both Streamable HTTP and SSE MCP endpoints. All tools, resources and prompts that can be accessed via the Streamable HTTP and SSE MCP endpoints are controlled by this policy. | 
| 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. | 
| You are strongly encouraged to enable Cross-Origin Resource Sharing filter for Streamable HTTP MCP server endpoints to accept request from the trusted origins only, for example: Example  CORS configuration
 | 
Typically, to enforce the HTTP security policy configuration, you can use the Quarkus OIDC extension.
For example, to verify bearer access tokens against an OIDC provider such as Keycloak, you can add the following configuration:
Quarkus OIDC configurationquarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus (1)
quarkus.oidc.token.audience=quarkus-mcp-server (2)
quarkus.oidc.resource-metadata.enabled=true (3)| 1 | Keycloak realm address. Replace it with your own provider’s address. | 
| 2 | Require that only access tokens that have a quarkus-mcp-serveraudience can be accepted. Change this audience value to uniquely identify your Quarkus MCP HTTP Server deployment. | 
| 3 | Allow MCP Clients to discover OAuth2 Protected Resource Metadata of this Quarkus MCP Server in order to follow the MCP Authorization OAuth2 Flow. See Quarkus OIDC Expanded Configuration Guide for more details. | 
Development mode
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/inspectorThe 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/inspectorHowever, --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}/ssefor HTTP/SSE; i.e./mcpand/mcp/sseby default. | 
$ docker run --rm -p 5173:5173 node:18 npx @modelcontextprotocol/inspectorTesting
The io.quarkiverse.mcp:quarkus-mcp-server-test artifact provides a set of convenient utils to test your MCP server.
The starting point is the io.quarkiverse.mcp.server.test.McpAssured class.
First, you need to create a new test client.
Currently, the clients for SSE and Streamable HTTP transports are supported.
The workflow continues as follows.
You create a group of MCP requests and corresponding assert functions.
The MCP requests are sent immediately but the responses are not processed and the assert functions are not used until the McpAssert#thenAssertResults() method is called.
In other words, the McpAssert#thenAssertResults() blocks and waits for all results; then all assert functions are applied.
You can create as many groups as you need.
A typical test for a @Tool may look like:
McpAssured Examplepackage org.acme;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Map;
import org.junit.jupiter.api.Test;
import io.quarkiverse.mcp.server.test.McpAssured;
import io.quarkiverse.mcp.server.test.McpAssured.McpStreamableTestClient;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class ToolsAnswerTest {
    @Test
    public void testAnswer() {
        McpStreamableTestClient client = McpAssured.newConnectedStreamableClient(); (1)
        client.when()
                .toolsCall("theAnswer", Map.of("lang", "Java"), r -> { (2)
                    assertEquals("Spaces are better for indentation.", r.content().get(0).asText().text());
                })
                .toolsCall("theAnswer", Map.of("lang", "python"), r -> {
                    assertEquals("Tabs are better for indentation.", r.content().get(0).asText().text());
                })
                .thenAssertResults(); (3)
    }
}| 1 | Create and connect a client. If you need to configure the client use the builder returned by McpAssured#newStreamableClient()instead. | 
| 2 | Send an MCP request and register an assert function. | 
| 3 | Wait for all responses and assert the results. | 
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 stdiotransport 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 bravoserver. | 
| 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 bravoserver, 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#btoolis bound to the server configurationbravo, i.e. it’s exposed with the/bravo/mcpendpoint. It’s not available at/alpha/mcp. | 
| The programmatic API also makes it possible to set the server configuration name; e.g. ToolDefinition#setServerName(). | 
Extension configuration reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
| Configuration property | Type | Default | 
|---|---|---|
| If set to  Environment variable:  | boolean | 
 | 
| 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 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 | 
 | 
| 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 | 
 | 
| The strategy used when server features, such as tools, prompts, and resources, reference an non-existent server name. Environment variable:  | 
 | 
 | 
| 
 The name of the server is included in the response to an  By default, the value of the  Environment variable:  | string | |
| 
 The version of the server is included in the response to an  By default, the value of the  Environment variable:  | string | |
| 
 The human-readable name of the server is included in the response to an  Environment variable:  | string | |
| 
 The instructions describing how to use the server and its features. These are hints for the clients. 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. The pagination is disabled if set to a value less or equal to zero. Environment variable:  | int | 
 | 
| 
 If the number of resource templates exceeds the page size then pagination is enabled and the given page size is used. The pagination is disabled if set to a value less or equal to zero. Environment variable:  | int | 
 | 
| 
 If the number of tools exceeds the page size then pagination is enabled and the given page size is used. The pagination is disabled if set to a value less or equal to zero. Environment variable:  | int | 
 | 
| 
 If set to  Environment variable:  | boolean | 
 | 
| 
 If the number of prompts exceeds the page size then pagination is enabled and the given page size is used. The pagination is disabled if set to a value less or equal to zero. 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:  | 
 | |
| 
 The default timeout for an elicitation request. 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 | 
 |