IntroductionIn the Use Quarkus MCP client to access secure MCP HTTP servers blog post, we explained how a user can login to Quarkus LangChain4j AI server application with GitHub OAuth2 and have Google AI Gemini use Quarkus MCP Client to access a secure Quarkus MCP Server user name provider tool with a GitHub access token.However, not every AI service application is going to be designed to require a user login: for example, it may run as a command line application or cron scheduler. But also, not every AI service application that requires a user login will be able to use a user login token to access a secure MCP server because such a server may only support tokens of different type.In this blog post, we will explain how Quarkus MCP Client that runs in a command line Quarkus LangChain4j AI application can itself acquire an access token using an OAuth2 client_credentials grant and use it to access a secure Quarkus MCP Server service account name provider tool.We will work with Keycloak and rely on it to demonstrate how to approach securing complex, distributed AI applications that may span multiple security boundaries, by requiring that access tokens are restricted to specific audiences, and exchanging them to acquire new, correct audiences.Demo architectureAs you can see in the diagram above, a command line agent uses a Poem Service AI service to create a poem. The Poem Service uses AI Gemini and requests MCP Client to complete a tool call to help AI Gemini to find out the service account name.The MCP client must use an access token. It uses an OAuth2 client_credential grant to acquire a service account token and propagate it to the secure MCP server. This service account token’s audience restricts it to accessing the MCP server only.The MCP server tool implementation must access a REST server to complete the tool action. However, it can not use the current access token that is restricted to accessing this MCP server because the REST server accepts tokens that are meant to access this REST server only.Therefore, the MCP server exchanges the current token to set the REST server audience before propagating it, with the REST server successfully completing the secure tool call, with the response returned to the MCP Client.We are now ready to start working on the demo.You can find the complete project source in the Quarkus LangChain4j Command Line Secure MCP Client Server sample.Step 1 - Create and start MCP serverFirst, let’s create a secure Quarkus MCP SSE server that can enforce an authenticated access to its tool, verify that the access token has a correct audience, and complete a tool action by exchanging the current access token for a new access token with the REST server audience and propagating this token to the REST server to get the required service account name.MCP server maven dependenciesAdd the following dependencies: io.quarkiverse.mcp quarkus-mcp-server-sse (1) 1.4.0 io.quarkus quarkus-oidc (2) io.quarkus quarkus-rest (3) io.quarkus quarkus-rest-client-oidc-token-propagation (4)1quarkus-mcp-server-sse is required to support MCP Streamable HTTP and SSE transports.2quarkus-oidc is required to secure access to MCP SSE endpoints. Its version is defined in the Quarkus BOM.3quarkus-rest is required to support REST server that the MCP tool has to call. Its version is defined in the Quarkus BOM.4quarkus-rest-client-oidc-token-propagation also brings quarkus-rest-client and is required to support a REST client call to REST server with the token exchange and propagation. Its version is defined in the Quarkus BOM.MCP Service Account Name ToolLet’s create a tool that can return a name of the current service account.package io.quarkiverse.langchain4j.sample;import org.eclipse.microprofile.rest.client.inject.RestClient;import io.quarkiverse.mcp.server.TextContent;import io.quarkiverse.mcp.server.Tool;import jakarta.inject.Inject;public class ServiceAccountNameProvider { (1) @RestClient @Inject ServiceAccountNameRestClient serviceAccountNameRestClient; (2) @Tool(name = "sevice-account-name-provider", description = "Provides a name of the current service account") (1) TextContent provideServiceAccountName() { return new TextContent(serviceAccountNameRestClient.getServiceAccountName()); (2) }}1Provide a tool that can return a name of the current service account.2Use an injected ServiceAccountNameRestClient to access the REST server to complete the service account name request. See the Service Account Name REST client section below for more details.The MCP server tool can be invoked only if the current MCP request is authenticated.In this blog post we do not enforce the secure tool access with annotations such as @PermissionAllowed or @Authenticated but only use the HTTP security policy configuration instead.See how both main MCP SSE and tool endpoints are secured in the MCP Server Configuration section below.Service Account Name REST clientThe MCP Service Account Name Tool uses the Service Account Name REST client to call the REST server to complete a service account name request.This REST client looks like this:package io.quarkiverse.langchain4j.sample;import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;import io.quarkus.oidc.token.propagation.common.AccessToken;import jakarta.ws.rs.GET;import jakarta.ws.rs.Produces;@RegisterRestClient@AccessToken (2)public interface ServiceAccountNameRestClient { @GET @Produces("text/plain") String getServiceAccountName(); (1)}1Get a service account name from the REST server. See the Service Account Name REST server section below for more details.2Use @AccessToken annotation to require the access token exchange and propagation. This single @AccessToken annotation, supported by an additional configuration in the MCP Server Configuration section below, is all that is required to support this complex access token flow.Service Account Name REST serverThe MCP Service Account Name Tool uses the Service Account Name REST client to get a service account name from the Service Account Name REST server.This REST server looks like this:package io.quarkiverse.langchain4j.sample;import io.quarkus.security.Authenticated;import io.quarkus.security.identity.SecurityIdentity;import jakarta.inject.Inject;import jakarta.ws.rs.GET;import jakarta.ws.rs.Path;import jakarta.ws.rs.Produces;@Path("/service-account-name")public class ServiceAccountNameRestServer { @Inject SecurityIdentity securityIdentity; @GET @Produces("text/plain") @Authenticated (1) public String getServiceAccountName() { return securityIdentity.getPrincipal().getName(); (2) }}1Provide a secure REST resource method that can return a service account name2Use an injected SecurityIdentity to complete the method’s task, in this case - return a service account identity name.In this demo, the REST server is collocated with the MCP server to simplify the demo. Of course, in production, such REST servers will most likely be remote.Next, let’s have a look, in the MCP Server Configuration section, how access to both the MCP Service Account Name Tool and this server is restricted to tokens with specific audiences only.MCP Server ConfigurationLet’s configure our secure MCP server:# MCP serverquarkus.mcp.server.server-info.name=Service Account Name Provider (1)quarkus.mcp.server.traffic-logging.enabled=truequarkus.mcp.server.traffic-logging.text-limit=1000# Require an authenticated access to MCP serverquarkus.http.auth.permission.authenticated.paths=/mcp/* (2)quarkus.http.auth.permission.authenticated.policy=authenticated# Default Quarkus OIDC tenant that verifies access tokens which reach the MCP server.quarkus.oidc.client-id=quarkus-mcp-server (3)quarkus.oidc.token.audience=quarkus-mcp-server (4)# Request a token exchange before the token propagationquarkus.rest-client-oidc-token-propagation.exchange-token=true (4)# OIDC client that performs the current token exchangequarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url} (5)quarkus.oidc-client.client-id=${quarkus.oidc.client-id}quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret}quarkus.oidc-client.scopes=quarkus-mcp-service-scopequarkus.oidc-client.grant.type=exchangequarkus.oidc-client.grant-options.exchange.subject_token_type=urn:ietf:params:oauth:token-type:access_token (6)# REST client which accesses a protected REST server, by propagating the exchanged tokenio.quarkiverse.langchain4j.sample.ServiceAccountNameRestClient/mp-rest/url=http://localhost:8080/service-account-name (7)# OIDC `service-account-name-rest-server` tenant that secures a protected REST server.quarkus.oidc.service-account-name-rest-server.auth-server-url=${quarkus.oidc.auth-server-url} (8)quarkus.oidc.service-account-name-rest-server.token.audience=quarkus-mcp-service (9)quarkus.oidc.service-account-name-rest-server.tenant-paths=/service-account-name# Keycloak devservice that enables a default OIDC tenant that secures MCP server.quarkus.keycloak.devservices.image-name=quay.io/keycloak/keycloak:26.3.1 (10)quarkus.keycloak.devservices.port=8081# Keycloak may require more memory on some systemsquarkus.keycloak.devservices.container-memory-limit=1250M1Declare MCP server and enable traffic logging.2Enforce an authenticated access to the main MCP SSE and tool endpoints. The configured pattern covers both the initial '/mcp/sse' handshake and '/mcp/messages/' requests.3Default OIDC tenant that secures the MCP SSE endpoint and tool. It is supported by Keycloak Dev Service in dev mode. In simple cases you do not even have to configure the default OIDC tenant. But in this demo, the default OIDC tenant is required to enforce that the tokens which reach the MCP server contain a quarkus-mcp-server audience.4Request an access token exchange before the Service Account Name REST client propagates it.5Configure OIDC client to perform the token exchange6Set the type of a new token that the current token will be exchanged for to access_token. Starting from Quarkus 3.25, an expected new token type will be set to access_token by default, and users will not have to configure this property when the access token type is required when exchanging tokens.7Configure the Service Account Name REST client with the REST server address. The REST server is collocated with the MCP server only to simplify the demo.8The OIDC tenant that protects the REST server only.9The OIDC tenant that protects the REST server requires that the tokens that are used to access it contain a REST server quarkus-mcp-service audience.10Configure Keycloak dev service to use one of the latest released Keycloak images, and make it run on a fixed 8081 port to simplify the Poem Service Configuration where an access to Keycloak is also required.Start the MCP server in dev modeNow let’s start the MCP server in dev mode:mvn quarkus:devand go to the Step 2 - Keycloak setup in the next section to complete the Keycloak configuration that is required to support the secure MCP server token audience and exchange requirements.Step 2 - Keycloak setupWhen we started the MCP server in dev mode, Keycloak Dev Service launched a Keycloak container, made it available on port 8081, created a quarkus realm with the quarkus-mcp-server client - this client name was configured with the quarkus.oidc.client-id=quarkus-mcp-server property in the MCP Server Configuration section.The quarkus-mcp-server client represents a confidential OIDC client that protects the MCP server.But MCP server and REST server have additional token audience and exchange requirements and we must complete the Keycloak setup to support those requirements. Let’s do it.Go to http://localhost:8081 and login as a Keycloak admin, with the admin name and admin password credentials.Select the quarkus realm:First, create a quarkus-mcp-client OIDC client that the Quarkus MCP client will use to acquire OAuth2 client_credentials tokens for accessing the MCP server.Start with the General Settings:and enable Client authentication and Service accounts roles capabilities:Save the quarkus-mcp-client OIDC client. Click on its Credentials tab and copy the generated secret to export it later as the OIDC client secret in order to run the command line AI Poem Service application.For the Quarkus MCP client to be able to access MCP server with access tokens that the quarkus-mcp-client OIDC client will acquire, these tokens must contain an audience (aud) claim with a quarkus-mcp-server audience. The MCP server is configured in the MCP Server Configuration section to require this audience.Keycloak supports several options for adding an audience (aud) claim to issued tokens. We will use an option that involves creating a custom Client scope with an Audience mapping.Go to the Client scopes and create an Optional quarkus-mcp-server-scope:Once the quarkus-mcp-server-scope scope is created, go to its Mappings tab, and choose Configure a new mapper option and select Audience:Name this mapper as quarkus-mcp-server-as-audience and choose quarkus-mcp-server as an Included Client Audience:Once the quarkus-mcp-server-scope is created, add it as an Optional scope to the quarkus-mcp-client:Now, when Quarkus MCP client will use the quarkus-mcp-client OIDC client to acquire tokens, it will request a quarkus-mcp-server-scope token scope, resulting in Keycloak issuing tokens with an audience that contains the quarkus-mcp-server - exactly what the Quarkus MCP server requires.Next, we need to support Quarkus MCP server exchanging the incoming access token with the quarkus-mcp-server audience for a new token that will contain a REST server audience instead.Create a quarkus-mcp-service OIDC client that represents the REST server, similarly to how you created the quarkus-mcp-client OIDC client. Next, create a quarkus-mcp-service-scope client scope, similarly to how you created the quarkus-mcp-server-scope client scope, choosing the quarkus-mcp-service as an Included Client Audience when creating an audience mapping for this scope.Once the quarkus-mcp-service-scope is created, add it as an Optional client scope to the quarkus-mcp-server MCP Server OIDC client, similarly to how you added the quarkus-mcp-server-scope to the quarkus-mcp-client above.Finally, update the quarkus-mcp-server capability to support a Standard Token Exchange, see the How to enable token exchange example in the Keycloak documentation.Now, the quarkus-mcp-server OIDC client that secures the MCP server can also exchange the incoming token and request a new quarkus-mcp-service audience by adding the quarkus-mcp-service-scope scope to the token exchange grant request, exactly what the REST server requires.If you actively work with another OAuth2 provider that can produce tokens with required audiences and exchange them using a standard token exchange grant, then you can also try to adapt this demo to work with that provider instead.Step 3 - Create and run Poem Service from command lineThe MCP server is now running and ready to accept tool calls. Let’s create a command line AI Poem Service that will work with AI Gemini and use Quarkus MCP client to complete tool calls.Poem Service Maven dependenciesAdd the following dependencies: io.quarkiverse.langchain4j quarkus-langchain4j-ai-gemini (1) io.quarkiverse.langchain4j quarkus-langchain4j-mcp (2) io.quarkiverse.langchain4j quarkus-langchain4j-oidc-client-mcp-auth-provider (3) io.quarkus quarkus-picocli (4)1quarkus-langchain4j-ai-gemini brings support for AI Gemini.2quarkus-langchain4j-mcp provides core MCP Client support.3quarkus-langchain4j-oidc-cient-mcp-auth-provider provides an implementation of McpClientAuthProvider that can supply access tokens that it itself acquires with an OAuth2 client_credentials grant (or any other supported grant that does not require a user input). Note, this dependency is different from the quarkus-langchain4j-oidc-mcp-auth-provider one that supplies tokens already available after an authorization code flow completes, it was demoed in the Use Quarkus MCP client to access secure MCP HTTP servers blog post to propagate GitHub login access tokens.4quarkus-picocli supports building command-line Quarkus applications. Its version is defined in the Quarkus BOM.AI Gemini API keyPoem Service relies on AI Gemini to create a poem.Get AI Gemini API key and export it as an AI_GEMINI_API_KEY environment property.OIDC client secretQuarkus MCP client will use an implementation of McpClientAuthProvider provided by the quarkus-langchain4j-oidc-cient-mcp-auth-provider dependency.This McpClientAuthProvider uses the configured OIDC client to acquire access tokens using an OAuth2 client_credentials grant, where an OIDC client secret must be provided.Export the OIDC quarkus-mcp-client client secret that you copied when working through the Step 2 - Keycloak setup section as an OIDC_CLIENT_SECRET environment property.Poem ServicePoem Service is a simple Quarkus LangChain4j AI service:package io.quarkiverse.langchain4j.sample;import dev.langchain4j.service.UserMessage;import io.quarkiverse.langchain4j.RegisterAiService;import io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;@RegisterAiServicepublic interface PoemService { @UserMessage(""" Write a short 1 paragraph poem in {language} about a Java programming language. Provide a translation to English if the original poem language is not English. Dedicate the poem to the service account, refer to this account by its name.""") (1) @McpToolBox("service-account-name") (2) String writePoem(String language); (1)}1Request to write a poem about Java.2Use Quarkus MCP service-account-name client configured in the Poem Service Configuration section to call a tool that can provide a service account name.This service is called from the PoemCommand:package io.quarkiverse.langchain4j.sample;import java.util.concurrent.Callable;import jakarta.enterprise.context.control.ActivateRequestContext;import jakarta.inject.Inject;import picocli.CommandLine.Command;import picocli.CommandLine.Option;@Command(name = "poem", mixinStandardHelpOptions = true, description = "Create a poem", version = "v1.0")@ActivateRequestContextpublic class PoemCommand implements Callable { @Option(names = { "-l", "--language" }, description = "Poem language", defaultValue = "English") String poemLanguage; @Inject PoemService poemService; @Override public Integer call() { System.out.println(poemService.writePoem(poemLanguage)); (1) return 0; }}1Call PoemService.Poem Service ConfigurationLet’s see how the command line Poem Service configuration looks like:quarkus.langchain4j.mcp.service-account-name.transport-type=http (1)quarkus.langchain4j.mcp.service-account-name.url=http://localhost:8081/mcp/sse/ (2)quarkus.oidc-client.auth-server-url=http://localhost:8081/realms/quarkus (3)quarkus.oidc-client.client-id=quarkus-mcp-client (4)quarkus.oidc-client.credentials.secret=${oidc_client_secret} (5)quarkus.oidc-client.scopes=quarkus-mcp-server-scope (6)quarkus.langchain4j.ai.gemini.api-key=${ai_gemini_api_key} (7)quarkus.langchain4j.ai.gemini.log-requests=true (8)quarkus.langchain4j.ai.gemini.log-responses=true1Enable MCP client HTTP transport. In this demo we use SSE, but Streamable HTTP is also supported.2Point to the Quarkus MCP server endpoint that you started in the Start the MCP server in dev mode step.3Configure OIDC client to acquire access tokens using OAuth2 client_credentials grant, a default grant type supported by the OIDC client. OIDC client points to a Keycloak quarkus realm, note the fixed 8081 port that you requested Keycloak Dev Service to use for Keycloak in the Step 2 - Keycloak setup section.4OIDC client id, you created the OIDC quarkus-mcp-client client in the Step 2 - Keycloak setup section.5OIDC quarkus-mcp-client client secret that you exported during the OIDC client secret step.6Request that the tokens issued to quarkus-mcp-client must contain a quarkus-mcp-server MCP server audience. You created a client quarkus-mcp-server-scope scope with a quarkus-mcp-server client audience mapping in the Step 2 - Keycloak setup section.7AI Gemini key that you acquired and exported during the AI Gemini API key step.8Enable AI Gemini request and response loggingPlease pay attention to the fact that the MCP client configuration has a service-account-name name. You referred to this configuration with the @McpToolBox("service-account-name") annotation in the Poem Service section.Package Poem ServicePackage the command line Poem Service:mvn clean packageRun Poem ServiceRun the command line Poem Service that you packaged in the Package Poem Service section:java -jar target/quarkus-app/quarkus-run.jarYou should get a response such as:For service-account-quarkus-mcp-client, this Java ode I write,A language strong, with classes bright, and objects shining light.From simple apps to systems grand, its power knows no end,With threads and streams, a helping hand, a journey without bend.Its virtual machine, a sturdy friend, on which great feats depend.How about trying another language ?java -jar target/quarkus-app/quarkus-run.jar --language GreekYou should get a response such as:Here's a short poem in Greek about Java, dedicated to the service account "service-account-quarkus-mcp-client":**Greek:**Ω, Java, γλώσσα ισχυρή και γρήγορη,για προγραμματισμό, εργαλείο ακριβές.Στον service-account-quarkus-mcp-client αφιερωμένη,η δύναμή σου, πάντα αξιοθαύμαστη.**English Translation:**O Java, language strong and fast,For programming, a precise tool.Dedicated to service-account-quarkus-mcp-client,Your power, always admirable.Have token audiences made any difference ?For the command line Poem Service to run successfully, Quarkus MCP client had to acquire a token with a quarkus-mcp-server audience to access the MCP server.Here is how a token that Keycloak issues to the MCP client looks like:The token aud claim contains two audience values, one of them is a required quarkus-mcp-server audience.For the MCP quarkus-mcp-server server to complete the Quarkus MCP client request, it had to verify that the token had a correct quarkus-mcp-server audience, and exchange it for a new token with a quarkus-mcp-service audience to access the REST server.Here is how an exchanged token that a Keycloak issues to the MCP server looks like:The token aud claim contains a required quarkus-mcp-service audience.Note this token still retains a record of the original quarkus-mcp-client client that acquired the previous token, but also lists quarkus-mcp-server as the authorizing party (azp).Let’s try to access both MCP server and REST server without an audience claim.Ensure the MCP server is running and Keycloak is configured.In the demo, the OIDC quarkus-mcp-client client acquires tokens that are used to access the MCP server.Use the following curl command to acquire a client_credentials token for the quarkus-mcp-client client, omitting a quarkus-mcp-server-scope grant property:curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials&client_id=quarkus-mcp-client&client_secret=keycloak_quarkus_mcp_client_secret" http://localhost:8081/realms/quarkus/protocol/openid-connect/tokenand confirm at jwt.io that the returned JWT token has no audience claim.Try to access the MCP server with this token:curl -H "Authorization: Bearer " http://localhost:8080/mcp/sseand you will get HTTP 401.What about the REST server ? In the demo, the OIDC quarkus-mcp-server client acquires tokens that are used to access the REST server.Use the following curl command to acquire a client_credentials token for the quarkus-mcp-server client, omitting a quarkus-mcp-service-scope grant property:curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials&client_id=quarkus-mcp-server&client_secret=secret" http://localhost:8081/realms/quarkus/protocol/openid-connect/tokenand confirm at jwt.io that the returned JWT token has no audience claim.Try to access the REST server with this token:curl -H "Authorization: Bearer " http://localhost:8080/service-account-nameand you will get HTTP 401.You can also enforce a stricter verification by requiring that tokens received by both MCP and REST servers were issued to the quarkus-mcp-client and quarkus-mcp-server respectively by adding the following configuration fragment to the MCP Server Configuration:# Tokens that are accepted by MCP server must have been requested by `quarkus-mcp-client`quarkus.oidc.token.required-claims.azp=quarkus-mcp-client# Tokens that are accepted by REST server must have been requested by `quarkus-mcp-server`quarkus.oidc.service-account-name-rest-server.token.required-claims.azp=quarkus-mcp-serverNote about Resource IndicatorsThe latest 2025-06-18 MCP authorization specification requires the use of OAuth2 Resource Indicators.OAuth2 Resource Indicator allows for a fine grained token audience restriction, in the presence of multiple, diverse resource servers that must be accessed with tokens.For a simple demo that we created in this blog post, having a token to contain an audience only is sufficient.If your provider already supports OAuth2 Resource Indicators and you need to have a token to also include a resource indicator, configure OIDC client to request it.For example, you can add quarkus.oidc-client.grant.client.extra-params.resource=http://localhost:8080/mcp to the Poem Service Configuration.In this case, to have the MCP server verify that an access token contains a correct resource indicator, add quarkus.oidc.token.required-claims.resource=http://localhost:8080/mcp to the MCP Server Configuration.Security ConsiderationsEnsuring that each participant in your distributed AI system is properly secured and accepts tokens thar are meant to access this participant only is crucial.Token audience restriction is one of the key OAuth2 mechanisms that supports this goal, with resource indicators allowing to achieve a finer-grained audience restriction.Token exchange can help to correctly switch the OAuth2 security context when the tokens are flowing in a multi-hop distributed AI application.Read more about the Access Token Privilege Restriction in the latest 2025-06-18 MCP authorization specification.ConclusionIn this blog post, we demonstrated how Quarkus MCP Client can access secure MCP servers by acquiring access tokens using an OAuth2 client_credentials grant and propagating them to the secure Quarkus MCP server.We also looked into restricting tokens to specific audiences and started learning about an important OAuth2 token exchange grant.We have more content dedicated to AI and MCP security lined up for you, stay tuned !