Quarkus build-time deployment extension for Model Context Protocol (MCP) client integration, handling configuration processing, synthetic bean generation, and framework integration
CDI qualifiers and functional annotations for MCP client selection and integration.
Qualifiers are used to select specific CDI beans when multiple beans of the same type exist.
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();
}
}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();
}
}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 provide behavior to application code.
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:
quarkus.langchain4j.mcp.generate-tool-provider=true (default)@RegisterAiService interfacesError 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);
}Authentication providers are resolved using the following rules:
@McpClientName("client-name"), use it for that client@McpClientName annotations, use it for all specified clients@McpClientName, use it as default for clients without specific providerResolution 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)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 runtimeAnnotations are processed during Quarkus build:
quarkus.langchain4j.mcp.* properties@McpClientName and @McpRegistryClientName instances createdAnnotations are used at runtime:
@McpToolBox filters tools for AI service methods@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;
}
}@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
}
}@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);
}
}
}@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