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.

Supported transports

MCP currently defines two standard transports for client-server communication. This extension supports both transports and defines a unified API to define server features. 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.Beta2</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/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.Beta2</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, 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 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 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 response has no description and contains the single message object.

  • If the method returns a List of PromptMessages then the response has no description and contains the list of messages.

  • If it returns any other type X then X is encoded using the PromptResponseEncoder API.

  • It may also return a Uni that 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:

  • 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 response contains a single value.

  • If the method returns a List of `String`s then the response contains the list of values.

  • The method may return a Uni that 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:

  • McpConnection

  • McpLog

  • RequestId

Programmatic API

It’s also possible to register a prompt programmatically with the PromptManager API.

import io.quarkiverse.mcp.server.PromptManager;
import jakarta.inject.Inject;

public class MyPrompts {

    @Inject
    PromptManager promptManager; (1)

    @Inject
    CodeService codeService;

    void addPrompt() {
       promptManager.newPrompt("code_assist") (2)
          .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(); (3)
    }
}
1 The injected manager can be used to obtain metadata and register a new prompt programmatically.
2 The PromptManager#newPrompt(String) method returns PromptDefinition - a builder-like API.
3 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 ResourceContents then the response contains the single contents object.

  • If the method returns a List of ResourceContents implementations then the response contains the list of contents objects.

  • If it returns any other type X or List<X> then X is encoded using the ResourceContentsEncoder API and afterwards the rules above apply.

  • It may also return a Uni that 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:

  • McpConnection

  • McpLog

  • RequestId

  • RequestUri

Programmatic API

It’s also possible to register a resource programmatically with the ResourceManager API.

import io.quarkiverse.mcp.server.ResourceManager;
import jakarta.inject.Inject;

public class MyResources {

    @Inject
    ResourceManager resourceManager; (1)

    void addResource() {
       resourceManager.newResource("file:///project/alpha") (2)
          .setDescription("Alpha resource file")
          .setHandler(
              args -> new ResourceResponse(
                                    List.of(BlobResourceContents.create(args.requestUri().value(), Files.readAllBytes(Paths.ALPHA)))))
          .register(); (3)
    }
}
1 The injected manager can be used to obtain metadata and register a new resource programmatically.
2 The ResourceManager#newResource(String) method returns ResourceDefinition - a builder-like API.
3 Registers the resource definition and sends the notifications/resources/list_changed notification to all connected clients.

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 response contains the single contents object.

  • If the method returns a List of ResourceContents implementations then the response contains the list of contents objects.

  • If it returns any other type X or List<X> then X is encoded using the ResourceContentsEncoder API and afterwards the rules above apply.

  • It may also return a Uni that 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:

  • McpConnection

  • McpLog

  • RequestId

  • RequestUri

Complete 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 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 response contains a single value.

  • If the method returns a List of `String`s then the response contains the list of values.

  • The method may return a Uni that 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:

  • McpConnection

  • McpLog

  • RequestId

  • RequestUri

Programmatic API

It’s also possible to register a resource template programmatically with the ResourceTemplateManager API.

import io.quarkiverse.mcp.server.ResourceTemplateManager;
import jakarta.inject.Inject;

public class MyResourceTemplates {

    @Inject
    ResourceTemplateManager resourceTemplateManager; (1)

    @Inject
    FileService fileService;

    void addResourceTemplate() {
       resourceTemplateManager.newResourceTemplate("alpha") (2)
          .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(); (3)
    }
}
1 The injected manager can be used to obtain metadata and register a new resource template programmatically.
2 The ResourceTemplateManager#newResourceTemplate(String) method returns ResourceTemplateDefinition - a builder-like API.
3 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") 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.
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.String then the response is "success" and contains the single TextContent object.

  • If the method returns an implementation of io.quarkiverse.mcp.server.Content then the response is "success" and contains the single content object.

  • If the method returns a List of Content implementations or `String`s then the response 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 it returns java.lang.String then the response is "success" and contains a single TextContent.

  • If it returns an implementation of Content then the response is "success" and contains a single content object.

  • If it returns a List of Content implementations or strings then the response is "success" and contains a list of relevant content objects.

  • If it returns any other type X or List<X> then X is encoded using the ToolResponseEncoder and ContentEncoder API and afterwards the rules above apply.

  • It may also return a Uni that 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:

  • McpConnection

  • McpLog

  • RequestId

Programmatic API

It’s also possible to register a tool programmatically with the ToolManager API.

import io.quarkiverse.mcp.server.ToolManager;
import jakarta.inject.Inject;

public class MyTools {

    @Inject
    ToolManager toolManager; (1)

    void addTool() {
       toolManager.newTool("toLowerCase") (2)
          .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(); (3)
    }
}
1 The injected manager can be used to obtain metadata and register a new tool programmatically.
2 The ToolManager#newTool(String) method returns ToolDefinition - a builder-like API.
3 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.

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.

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.

Example server configuration
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.

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 npm 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.

$ 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 name of the server is included in the response to an initialize request as defined by the spec. By default, the value of the quarkus.application.name config property is used.

Environment variable: QUARKUS_MCP_SERVER_SERVER_INFO_NAME

string

The version of the server is included in the response to an initialize request as defined by the spec. By default, the value of the quarkus.application.version config property is used.

Environment variable: QUARKUS_MCP_SERVER_SERVER_INFO_VERSION

string

If set to true then JSON messages received/sent are logged.

Environment variable: QUARKUS_MCP_SERVER_TRAFFIC_LOGGING_ENABLED

boolean

false

The number of characters of a text message which will be logged if traffic logging is enabled.

Environment variable: QUARKUS_MCP_SERVER_TRAFFIC_LOGGING_TEXT_LIMIT

int

200

The default log level.

Environment variable: QUARKUS_MCP_SERVER_CLIENT_LOGGING_DEFAULT_LEVEL

debug, info, notice, warning, error, critical, alert, emergency

info

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: QUARKUS_MCP_SERVER_AUTO_PING_INTERVAL

Duration 

If set to true then if an MCP client attempts to reconnect an SSE connection but does not reinitialize properly, the server will perform a "dummy" initialization; capability negotiation and protocol version agreement is skipped.

Environment variable: QUARKUS_MCP_SERVER_DEV_MODE_DUMMY_INIT

boolean

true

If the number of resources exceeds the page size then pagination is enabled and the given page size is used.

Environment variable: QUARKUS_MCP_SERVER_RESOURCES_PAGE_SIZE

int

50

If the number of resource templates exceeds the page size then pagination is enabled and the given page size is used.

Environment variable: QUARKUS_MCP_SERVER_RESOURCE_TEMPLATES_PAGE_SIZE

int

50

If the number of tools exceeds the page size then pagination is enabled and the given page size is used.

Environment variable: QUARKUS_MCP_SERVER_TOOLS_PAGE_SIZE

int

50

If the number of prompts exceeds the page size then pagination is enabled and the given page size is used.

Environment variable: QUARKUS_MCP_SERVER_PROMPTS_PAGE_SIZE

int

50

About the Duration format

To write duration values, use the standard java.time.Duration format. See the Duration#parse() Java API documentation for more information.

You can also use a simplified format, starting with a number:

  • If the value is only a number, it represents time in seconds.

  • If the value is a number followed by ms, it represents time in milliseconds.

In other cases, the simplified format is translated to the java.time.Duration format for parsing:

  • If the value is a number followed by h, m, or s, it is prefixed with PT.

  • If the value is a number followed by d, it is prefixed with P.

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

If set to false then the stdio transport is completely disabled, i.e. the application does not read/write messages from/to the standard input/output.

Keep in mind that console logging is still automatically redirected to the standard error. You will need to set the quarkus.log.console.stderr to false to suppress this behavior.

Environment variable: QUARKUS_MCP_SERVER_STDIO_ENABLED

boolean

true

If set to true then the standard output stream is set to "null" when the app is started.

Environment variable: QUARKUS_MCP_SERVER_STDIO_NULL_SYSTEM_OUT

boolean

true

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

The SSE endpoint is exposed at {rootPath}/sse. By default, it’s /mcp/sse.

Environment variable: QUARKUS_MCP_SERVER_SSE_ROOT_PATH

string

/mcp