CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-mcp-deployment

Quarkus build-time deployment extension for Model Context Protocol (MCP) client integration, handling configuration processing, synthetic bean generation, and framework integration

Overview
Eval results
Files

annotations.mddocs/

Annotations Reference

CDI qualifiers and functional annotations for MCP client selection and integration.

Qualifier Annotations

Qualifiers are used to select specific CDI beans when multiple beans of the same type exist.

@McpClientName

Qualifier for selecting a specific MCP client bean or associating an authentication provider with specific clients.

package io.quarkiverse.langchain4j.mcp.runtime;

import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Qualifier annotation for MCP clients.
 * Used to:
 * - Inject specific MCP client by name
 * - Associate authentication provider with specific client(s)
 * - Observe log messages from specific client
 */
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
@Repeatable(McpClientNames.class)
public @interface McpClientName {
    /**
     * The MCP client name.
     * Must match a configured client name from quarkus.langchain4j.mcp.{name}.*
     *
     * @return Client name
     */
    String value();

    /**
     * Helper for programmatic qualifier creation.
     */
    class Literal extends AnnotationLiteral<McpClientName> implements McpClientName {
        /**
         * Factory method for creating Literal instances.
         *
         * @param value Client name
         * @return New Literal instance
         */
        public static Literal of(String value) {
            return new Literal(value);
        }

        private final String value;

        public Literal(String value) {
            this.value = value;
        }

        @Override
        public String value() {
            return value;
        }
    }
}

Usage - Injection:

import dev.langchain4j.mcp.client.McpClient;
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class McpService {
    @Inject
    @McpClientName("github")
    McpClient githubClient;

    @Inject
    @McpClientName("filesystem")
    McpClient filesystemClient;

    public void useClients() {
        var githubTools = githubClient.listTools();
        var fsTools = filesystemClient.listTools();
    }
}

Usage - Authentication Provider:

import io.quarkiverse.langchain4j.mcp.auth.McpClientAuthProvider;
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
import jakarta.enterprise.context.ApplicationScoped;

// Provider for single client
@ApplicationScoped
@McpClientName("github")
public class GitHubAuthProvider implements McpClientAuthProvider {
    @Override
    public String getAuthorization(Input input) {
        return "Bearer " + System.getenv("GITHUB_TOKEN");
    }
}

// Provider for multiple clients (repeatable annotation)
@ApplicationScoped
@McpClientName("server1")
@McpClientName("server2")
public class MultiClientAuthProvider implements McpClientAuthProvider {
    @Override
    public String getAuthorization(Input input) {
        String clientName = resolveClientName(input);
        return "Bearer " + getTokenForClient(clientName);
    }
}

Usage - Event Observation:

import dev.langchain4j.mcp.client.logging.McpLogMessage;
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

@ApplicationScoped
public class McpLogObserver {
    // Observe logs from specific client
    public void onGitHubLog(@Observes @McpClientName("github") McpLogMessage log) {
        System.out.println("GitHub MCP: " + log.data());
    }

    // Observe logs from all clients (no qualifier)
    public void onAnyLog(@Observes McpLogMessage log) {
        System.out.println("MCP Log: " + log.data());
    }
}

Programmatic Qualifier Creation:

import dev.langchain4j.mcp.client.McpClient;
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

public class DynamicMcpAccess {
    @Inject
    Instance<McpClient> clients;

    public McpClient getClient(String name) {
        // Using factory method (recommended)
        return clients.select(McpClientName.Literal.of(name)).get();
    }

    public McpClient getClientAlt(String name) {
        // Using constructor (alternative)
        return clients.select(new McpClientName.Literal(name)).get();
    }
}

@McpRegistryClientName

Qualifier for selecting a specific MCP registry client bean.

package io.quarkiverse.langchain4j.mcp.runtime;

import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Qualifier annotation for MCP registry clients.
 * Used to inject specific registry client by name.
 */
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface McpRegistryClientName {
    /**
     * The MCP registry client name.
     * Must match a configured registry name from quarkus.langchain4j.mcp.registry-client.{name}.*
     *
     * @return Registry client name
     */
    String value();

    /**
     * Helper for programmatic qualifier creation.
     */
    class Literal extends AnnotationLiteral<McpRegistryClientName>
            implements McpRegistryClientName {
        /**
         * Factory method for creating Literal instances.
         *
         * @param value Registry client name
         * @return New Literal instance
         */
        public static Literal of(String value) {
            return new Literal(value);
        }

        private final String value;

        public Literal(String value) {
            this.value = value;
        }

        @Override
        public String value() {
            return value;
        }
    }
}

Usage - Injection:

import dev.langchain4j.mcp.registryclient.McpRegistryClient;
import dev.langchain4j.mcp.registryclient.model.McpServerListRequest;
import io.quarkiverse.langchain4j.mcp.runtime.McpRegistryClientName;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class McpServerDiscovery {
    @Inject
    @McpRegistryClientName("official")
    McpRegistryClient registryClient;

    public void searchServers(String query) {
        var result = registryClient.listServers(
            McpServerListRequest.builder()
                .search(query)
                .limit(10L)
                .build()
        );

        result.getServers().forEach(server -> {
            System.out.println("Found: " + server.getServer().getName());
        });
    }
}

Programmatic Qualifier Creation:

import dev.langchain4j.mcp.registryclient.McpRegistryClient;
import io.quarkiverse.langchain4j.mcp.runtime.McpRegistryClientName;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

public class DynamicRegistryAccess {
    @Inject
    Instance<McpRegistryClient> registries;

    public McpRegistryClient getRegistry(String name) {
        // Using factory method (recommended)
        return registries.select(McpRegistryClientName.Literal.of(name)).get();
    }

    public McpRegistryClient getRegistryAlt(String name) {
        // Using constructor (alternative)
        return registries.select(new McpRegistryClientName.Literal(name)).get();
    }
}

@McpClientNames

Container annotation for repeatable @McpClientName annotations.

package io.quarkiverse.langchain4j.mcp.runtime;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Container annotation for repeatable @McpClientName.
 * Automatically applied when multiple @McpClientName annotations are used.
 */
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface McpClientNames {
    /**
     * Array of McpClientName annotations.
     *
     * @return Client name annotations
     */
    McpClientName[] value();
}

Usage: Automatically applied when using multiple @McpClientName annotations:

@ApplicationScoped
@McpClientName("client1")
@McpClientName("client2")
@McpClientName("client3")
public class MultiClientAuthProvider implements McpClientAuthProvider {
    // Compiler automatically creates @McpClientNames container
}

Functional Annotations

Functional annotations provide behavior to application code.

@McpToolBox

Annotation for AI service methods to select which MCP servers' tools should be available.

package io.quarkiverse.langchain4j.mcp.runtime;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Specifies which MCP clients' tools should be available to an AI service method.
 * Only works with declarative AI services (@RegisterAiService).
 *
 * Requires quarkus.langchain4j.mcp.generate-tool-provider=true (default).
 */
@Retention(RUNTIME)
@Target(METHOD)
public @interface McpToolBox {
    /**
     * Names of MCP clients whose tools should be available.
     * Empty array means all configured MCP clients.
     *
     * @return Array of MCP client names, or empty for all
     */
    String[] value() default {};
}

Usage - Single Client:

import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;

@RegisterAiService
public interface GitHubAssistant {
    @SystemMessage("You are a GitHub assistant.")
    @McpToolBox("github")  // Only use GitHub MCP tools
    String chat(@UserMessage String message);
}

Usage - Multiple Clients:

@RegisterAiService
public interface DevelopmentAssistant {
    @SystemMessage("You are a development assistant with access to GitHub and filesystem.")
    @McpToolBox({"github", "filesystem"})  // Use both GitHub and filesystem tools
    String chat(@UserMessage String message);
}

Usage - All Clients:

@RegisterAiService
public interface UniversalAssistant {
    @SystemMessage("You are a universal assistant with access to all available tools.")
    @McpToolBox  // Empty = all configured MCP clients
    String chat(@UserMessage String message);
}

Per-Method Tool Selection:

@RegisterAiService
public interface SmartAssistant {
    @McpToolBox("github")
    String helpWithGitHub(@UserMessage String message);

    @McpToolBox("filesystem")
    String helpWithFiles(@UserMessage String message);

    @McpToolBox({"github", "filesystem"})
    String helpWithBoth(@UserMessage String message);
}

Requirements and Constraints:

  1. Tool Provider Generation: Only works when quarkus.langchain4j.mcp.generate-tool-provider=true (default)
  2. Declarative AI Services: Only supported on @RegisterAiService interfaces
  3. Client Names: Names must match configured MCP clients
  4. Scope: Applied per method, not per class

Error Cases:

// ERROR: Non-existent client name
@McpToolBox("nonexistent")
String chat(@UserMessage String message);
// Results in runtime error: no such MCP client

// ERROR: Used with custom ToolProvider
@RegisterAiService(toolProviderSupplier = CustomProvider.class)
public interface CustomAssistant {
    @McpToolBox("github")  // Ignored! Custom provider used instead
    String chat(@UserMessage String message);
}

Annotation Resolution

Qualifier Resolution for McpClientAuthProvider

Authentication providers are resolved using the following rules:

  1. Specific Match: If provider has @McpClientName("client-name"), use it for that client
  2. Multiple Match: If provider has multiple @McpClientName annotations, use it for all specified clients
  3. Default Match: If provider has no @McpClientName, use it as default for clients without specific provider
  4. No Match: If no provider matches, no authorization header is added

Resolution Examples:

// Configuration
quarkus.langchain4j.mcp.github.transport-type=streamable-http
quarkus.langchain4j.mcp.gitlab.transport-type=streamable-http
quarkus.langchain4j.mcp.internal.transport-type=streamable-http

// Providers
@ApplicationScoped
@McpClientName("github")
class GitHubAuth implements McpClientAuthProvider { } // Used by 'github'

@ApplicationScoped
@McpClientName("gitlab")
class GitLabAuth implements McpClientAuthProvider { } // Used by 'gitlab'

@ApplicationScoped
class DefaultAuth implements McpClientAuthProvider { } // Used by 'internal' (no specific provider)

Qualifier Resolution for CDI Injection

Standard CDI qualifier resolution applies:

@Inject
McpClient client;  // ERROR: Ambiguous - multiple MCP clients exist

@Inject
@McpClientName("github")
McpClient client;  // OK: Specific qualifier

@Inject
@McpClientName("nonexistent")
McpClient client;  // ERROR: Unsatisfied dependency at runtime

Annotation Processing

Build-Time Processing

Annotations are processed during Quarkus build:

  1. Configuration Parsing: Client names from quarkus.langchain4j.mcp.* properties
  2. Qualifier Generation: @McpClientName and @McpRegistryClientName instances created
  3. Bean Generation: Synthetic beans created with appropriate qualifiers
  4. Validation: Configuration validated against annotation usage

Runtime Processing

Annotations are used at runtime:

  1. CDI Injection: Qualifiers resolve correct bean instances
  2. Event Observation: Qualifiers filter CDI events
  3. Tool Selection: @McpToolBox filters tools for AI service methods
  4. Auth Provider Resolution: Qualifiers match providers to clients

Common Patterns

Multi-Tenant Authentication

@ApplicationScoped
class TenantAwareAuthProvider implements McpClientAuthProvider {
    @Inject
    TenantContext tenantContext;

    @Override
    public String getAuthorization(Input input) {
        String tenant = tenantContext.getCurrentTenant();
        String token = getTokenForTenant(tenant);
        return "Bearer " + token;
    }
}

Dynamic Client Selection

@ApplicationScoped
public class DynamicMcpService {
    @Inject
    Instance<McpClient> clients;

    public McpClient selectClient(String name) {
        return clients.select(McpClientName.Literal.of(name)).get();
    }

    public void executeToolOnClient(String clientName, String toolName) {
        McpClient client = selectClient(clientName);
        // Execute tool
    }
}

Selective Log Monitoring

@ApplicationScoped
public class McpMonitor {
    // Only monitor production clients
    public void onProdLog(
        @Observes @McpClientName("prod-server1") McpLogMessage log1
    ) {
        alertOnError(log1);
    }

    // Monitor all clients for warnings
    public void onAnyWarning(@Observes McpLogMessage log) {
        if (log.level() == McpLogLevel.WARNING) {
            logWarning(log);
        }
    }
}

Conditional Tool Access

@RegisterAiService
public interface ContextAwareAssistant {
    // Public users: limited toolset
    @McpToolBox("public-tools")
    String chatPublic(@UserMessage String message);

    // Authenticated users: more tools
    @McpToolBox({"public-tools", "user-tools"})
    String chatAuthenticated(@UserMessage String message);

    // Admins: all tools
    @McpToolBox
    String chatAdmin(@UserMessage String message);
}

Install with Tessl CLI

npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-mcp-deployment@1.7.0

docs

annotations.md

build-time-api.md

configuration.md

index.md

runtime-integration.md

tile.json