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 a declarative API that enables developers to implement the MCP server features easily.
Supported transports
MCP currently defines two standard transports for client-server communication. This extension supports both transports and defines a unified declarative API. In other words, the server features are defined with 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.0.0.Alpha3</version>
</dependency>
If you use the stdio transport then your app should not write anything to the standard output. Logging in the console is automatically disabled. And the standard output stream is set to "null" when the app is started by default.
|
If you want to use the HTTP/SSE 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.0.0.Alpha3</version>
</dependency>
The SSE endpoint is exposed at /{rootPath}/sse . The {rootPath} is set to mcp by default but it can be changed with the quarkus.mcp.server.sse.root-path configuration property.
|
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) is represented by an annotated business method of a CDI bean.
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 activated 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
,@Blocking
or@Transactional
are considered blocking. -
Methods declared in a class annotated with
@RunOnVirtualThread
are considered blocking. -
Methods annotated with
@NonBlocking
are considered non-blocking. -
Methods declared in a class annotated with
@Transactional
are 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
Uni
andMulti
are considered non-blocking. -
Methods returning any other type are considered blocking.
-
-
Kotlin
suspend
functions are always considered non-blocking and may not be annotated with@Blocking
,@NonBlocking
or@RunOnVirtualThread
and 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
@RunOnVirtualThread
or in a class annotated with@RunOnVirtualThread
. -
Methods annotated with
@RunOnVirtualThread
or declared in class annotated with@RunOnVirtualThread
must execute on a virtual thread, each invocation spawns a new virtual thread.
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") 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 can be used to customize the description of an argument. |
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
PromptMessage
then the reponse has no description and contains the single message object. -
If the method returns a
List
ofPromptMessage
s then the reponse has no description and contains the list of messages. -
The method may return a
Uni
that wraps any of the type mentioned above.
In other words, the return type must be one of the following list:
-
PromptResponse
-
PromptMessage
-
List<PromptMessage>
-
Uni<PromptResponse>
-
Uni<PromptMessage>
-
Uni<List<PromptMessage>>
Method parameters
A @Prompt
method must only accept String
parameters that represent Prompt arguments.
However, it may also accept the following parameters:
-
McpConnection
-
McpLog
-
RequestId
Prompt complete
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 not such prompt exists then the build fails. |
2 | The method returns a list of matching values. |
3 | The @CompleteArg 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.String
then the reponse contains a single value. -
If the method returns a
List
of `String`s then the reponse contains the list of values. -
The method may return a
Uni
that wraps any of the type mentioned above.
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
ResourceContents
then the reponse contains the single contents object. -
If the method returns a
List
ofResourceContents
implementations then the reponse contains the list of contents objects. -
The method may return a
Uni
that wraps any of the type mentioned above.
Method parameters
A @Resource
method may accept the following parameters:
-
McpConnection
-
McpLog
-
RequestId
Resource templates
You can also use resource templates to expose parameterized resources.
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, String uri) { (5)
return TextResourceContents.create(uri, 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
ResourceContents
then the reponse contains the single contents object. -
If the method returns a
List
ofResourceContents
implementations then the reponse contains the list of contents objects. -
The method may return a
Uni
that wraps any of the type mentioned above.
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 not 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.String
then the reponse contains a single value. -
If the method returns a
List
of `String`s then the reponse contains the list of values. -
The method may return a
Uni
that wraps any of the type mentioned above.
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") String name) {
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. |
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.String
then the reponse is "success" and contains the singleTextContent
object. -
If the method returns an implementation of
io.quarkiverse.mcp.server.Content
then the reponse is "success" and contains the single content object. -
If the method returns a
List
ofContent
implementations or `String`s then the reponse is "success" and contains the list of relevant content objects. -
The method may return a
Uni
that wraps any of the type mentioned above.
If a method annotated with @Tool throws a io.quarkiverse.mcp.server.ToolCallException then the response "failure" and the message of the exception is used as the text of the result content.
|
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.
|
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=50 (2)
1 | Enables traffic logging. |
2 | Set the number of characters of a JSON message which will be logged. |
Extension configuration reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
---|---|---|
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 true then JSON messages received/sent are logged. 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: |
|
|
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
---|---|---|
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 SSE endpoint is exposed at Environment variable: |
string |
|