Implementing Tools

Tools are the primary way to expose functionality through an MCP server. MCP provides a standardized way for servers to expose tools that can be invoked by clients. This guide shows you how to implement tools using both declarative annotations and programmatic APIs.

MCP Tools are not called by the LLM directly. It uses the regular function calling mechanism. The MCP client is responsible for deciding when to call a tool based on the LLM’s output.

Basic Tool Implementation

import io.quarkiverse.mcp.server.TextContent;
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 your description here.") (3)
    ToolResponse foo(@ToolArg(description = "The name", defaultValue = "Andy") String name) { (4)
        return ToolResponse.success(
                new TextContent(fooService.ping(name)));
    }

}
1 The @Singleton scope is added automatically, if needed.
2 MyTools is an ordinary CDI bean. It can inject other beans, use interceptors, etc.
3 @Tool annotates a business method of a CDI bean that should be exposed as a tool. By default, the name of the tool is derived from the method name.
4 The @ToolArg annotation can be used to customize the description of an argument and set the default value that is used when a client does not provide an argument value.
Default values are specified as strings and automatically converted to the argument type. For complex types, you can implement custom DefaultValueConverter instances. See Default Value Converters for more information.
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.

Return Types

A result of a "tool call" operation is always represented as a ToolResponse. However, @Tool methods can return other types that are automatically converted.

For details on supported return types and conversion rules, see Return Type Conversion.

Method Parameters

A @Tool method can accept parameters with various types. The following additional parameters may also be injected:

io.quarkiverse.mcp.server.McpConnection

The connection from an MCP client.

io.quarkiverse.mcp.server.McpLog

Used to send log message notifications to the MCP client.

io.quarkiverse.mcp.server.RequestId

The identifier of the current MCP request.

io.quarkiverse.mcp.server.Progress

Used to send progress notification messages back to the client.

io.quarkiverse.mcp.server.Roots

Used to obtain the list of root objects from the MCP client.

io.quarkiverse.mcp.server.Sampling

Used to request LLM sampling from models.

io.quarkiverse.mcp.server.Elicitation

Used to request additional information from the client.

io.quarkiverse.mcp.server.Cancellation

Used to determine if an MCP client requested a cancellation of an in-progress request.

io.quarkiverse.mcp.server.RawMessage

Represents an unprocessed MCP request or notification from an MCP client.

io.quarkiverse.mcp.server.Meta

Additional metadata sent from the client to the server, i.e. the _meta part of the message.

See Parameter Types for complete details on supported parameter types.

Programmatic API

It’s also possible to register a tool programmatically with the io.quarkiverse.mcp.server.ToolManager API.

For example, if a tool is only known at application startup time, it can be added as follows:

import io.quarkiverse.mcp.server.ToolManager;
import io.quarkiverse.mcp.server.ToolResponse;
import io.quarkus.runtime.Startup;
import jakarta.inject.Inject;

public class MyTools {

    @Inject
    ToolManager toolManager; (1)

    @Startup (2)
    void addTool() {
       toolManager.newTool("toLowerCase") (3)
          .setDescription("Converts input string to lower case.")
          .addArgument("value", "Value to convert", true, String.class)
          .setHandler(
              ta -> ToolResponse.success(ta.args().get("value").toString().toLowerCase()))
          .register(); (4)
    }
}
1 The injected manager can be used to obtain metadata and register a new tool programmatically.
2 Ensure that addTool is executed when the application starts.
3 The ToolManager#newTool(String) method returns ToolDefinition, a builder-like API.
4 Registers the tool definition and sends the notifications/tools/list_changed notification to all connected clients.

The programmatic API also allows you to remove existing tools at runtime (using removeTool(String name)), which is not possible with the annotation-based approach.

LangChain4j Support

The @dev.langchain4j.agent.tool.Tool and @dev.langchain4j.agent.tool.P annotations from LangChain4j can be used instead of @Tool/@ToolArg. This allows you to reuse existing LangChain4j tool definitions in your MCP server.

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import io.quarkiverse.mcp.server.McpConnection;
import java.time.DayOfWeek;

public class MyTools {

    @Tool(name = "getDayInfo", value = { "Get information about a specific day" }) (1)
    String getDayInfo(@P(value = "day") DayOfWeek day, McpConnection connection) { (2)
        return DayOfWeek.MONDAY.equals(day)
            ? "Start of the work week"
            : "Another day";
    }
}
1 @dev.langchain4j.agent.tool.Tool with name and value (description) attributes
2 @dev.langchain4j.agent.tool.P annotates method parameters to specify their description
While LangChain4j annotations are supported, keep in mind that semantics may vary and follow 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 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 Configuration Reference for relevant configuration 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.InputSchemaGenerator;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.Tool.InputSchema;
import io.quarkiverse.mcp.server.ToolArg;

public class MyTools {

    @Tool(description = "Put your 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 MySchemaGenerator is used to generate the input schema for this @Tool method. InputSchemaGenerator implementations 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 built-in implementation.

Caching Generated JSON Schemas

If your application contains many tools with complex input/output schemas, it might make sense to cache the generated schemas so that they are not regenerated for each tools/list request.

You can leverage CDI decorators to implement a simple cache:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import io.quarkiverse.mcp.server.GlobalInputSchemaGenerator;
import io.quarkiverse.mcp.server.InputSchema;
import io.quarkiverse.mcp.server.ToolInfo;
import jakarta.annotation.Priority;
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.inject.Inject;

@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 @Priority enables the decorator. Decorators with smaller priority values are called first.
2 @Decorator marks 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. 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.