使用 Quarkus MCP 客户端访问安全的 MCP HTTP 服务器

简介

使用流式 HTTP或 HTTP/SSE 传输的 MCP 服务器可能需要 MCP 客户端身份验证。

在《为 Quarkus MCP 服务器的安全 MCP 做准备》这篇博文中,我们解释了如何通过 Quarkus MCP Server 强制执行 MCP 客户端身份验证,并演示了 MCP Server DevUI 如何在开发模式下使用 Keycloak 访问令牌访问 MCP 服务器,以及 MCP Inspectorcurl 如何在生产模式下使用 GitHub 访问令牌访问 MCP 服务器。

在这篇博文中,我们将解释 Quarkus MCP Client 如何使用访问令牌访问安全的 MCP 服务器。

我们将展示如何通过 GitHub OAuth2 登录 Quarkus LangChain4j AI Poem Service 应用程序,并让 Google AI Gemini 利用 Quarkus MCP Client 提供的工具调用,该客户端可以将 GitHub 访问令牌传递给安全的 Quarkus MCP Server。

演示架构

Poem Service Architecture

如上图所示,用户登录到 Quarkus REST Poem Service 应用程序端点。为了支持用户创建诗歌的请求,Poem Service 使用 AI Gemini 并请求 MCP Client 完成工具调用,以帮助 AI Gemini 获取登录用户的姓名。

一个关键点是,Poem ServiceMCP Client 都是同一个 Quarkus REST 应用程序的一部分,只有通过 GitHub 登录的用户才能访问。用户不直接登录 MCP Client,而是登录 Poem Service 应用程序,使用 MCP client 是该应用程序完成用户请求的实现细节。

因此,此演示不演示 MCP 授权流程的实现,该流程主要对作为单页应用程序 (SPA) 实现的公共 MCP 客户端(例如 Anthropic Claude)感兴趣,这些客户端将能够启动用户登录到导入的 MCP 服务器。

此演示展示了典型的 OAuth2 授权码流程,其中用户登录到 REST 端点并授权该端点代表用户访问另一个服务。它还加强了关于 AI 安全是应用程序安全不可或缺的一部分 的信息。

例如,让我们暂时更新此图,移除 AI Gemini,将 MCP Client 替换为 REST Client,将 MCP Server 替换为 Poem Creator service,并将 GitHub 替换为 OAuth2

Typical OAuth2 Authorization

您很可能会在自己项目的这张图与您在项目中做的事情之间找到相似之处。这是 OAuth2 授权码流程的实际应用:用户登录到应用程序并授权它代表用户访问另一个提供诗歌创作的服务。

该演示表明 Quarkus MCP Client 可以在此类架构中有效工作,能够利用用户登录期间获得的访问令牌,而无需您编写任何自定义代码。

现在我们准备开始处理 Secure MCP Client Server 演示。

您可以在 Quarkus LangChain4j Secure MCP Client Server 示例 中找到完整的项目源代码。

步骤 1 - 创建并启动 MCP 服务器

首先,让我们创建一个安全的 Quarkus MCP SSE 服务器。

如果您已经按照《为 Quarkus MCP 服务器的安全 MCP 做准备》博文中 所述创建了 MCP 服务器,那么下面的说明对您来说将很熟悉,并且应该能够重用您之前创建的项目,只需稍作更新。

MCP 服务器需要身份验证才能建立服务器发送事件 (SSE) 连接,并在调用工具时也需要。此外,提供工具访问的 MCP 服务器端点要求安全身份具有 read:name 权限。

MCP 服务器 Maven 依赖项

添加以下依赖项

<dependency>
    <groupId>io.quarkiverse.mcp</groupId>
    <artifactId>quarkus-mcp-server-sse</artifactId> (1)
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId> (2)
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId> (3)
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId> (3)
</dependency>
1 需要 quarkus-mcp-server-sse 来支持 MCP SSE 传输。
2 quarkus-oidc 是保护对 MCP SSE 端点的访问所必需的。它的版本在 Quarkus BOM 中定义。
3 需要 quarkus-hibernate-orm-panachequarkus-jdbc-postgresql 来支持 安全身份增强。它们的版本在 Quarkus BOM 中定义。

MCP 服务器工具

让我们创建一个可以返回当前登录用户姓名的工具。只有在当前 MCP 请求已通过身份验证,并且安全身份具有 read:name 权限的情况下才能调用它。

package io.quarkiverse.langchain4j.sample;

import io.quarkiverse.mcp.server.TextContent;
import io.quarkiverse.mcp.server.Tool;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.inject.Inject;

public class UserNameProvider {

    @Inject
    SecurityIdentity securityIdentity;

    @Tool(name = "user-name-provider", description = "Provides a name of the currently logged-in user") (1)
    @PermissionsAllowed("read:name") (2)
    TextContent provideUserName() {
        return new TextContent(securityIdentity.getPrincipal().getName()); (3)
    }
}
1 提供一个可以返回当前用户姓名的工具。
2 要求经过身份验证的工具访问,并附加一个 read:name 权限约束——是的,与未经验证的 MCP 服务器工具唯一的区别是 @PermissionsAllowed("read:name"),仅此而已!另请参阅下面“MCP 服务器配置”部分中如何保护主要的 MCP SSE 端点。
3 使用注入的 SecurityIdentity 返回当前用户的姓名。或者,也可以从注入的 quarkus.oidc.UserInfo 获取。

安全身份增强

为了满足 @PermissionsAllowed("read:name") 授权约束,在验证 GitHub 访问令牌后创建的安全身份必须得到增强,使其具有 read:name 权限。

该演示预期数据库中有一个记录了 GitHub 账户名称和分配的权限。安全身份增强器使用身份名称来检索此记录,并用发现的权限增强该身份。

让我们看看 Quarkus 如何轻松实现这项相当复杂的任务。

首先,我们创建一个 Panache 实体来存储账户名称和权限值。

package io.quarkiverse.langchain4j.sample;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

@Entity
public class Identity extends PanacheEntity {
    @Column(unique = true)
    public String name;
    public String permission;

    public static Identity findByName(String name) { (1)
        return find("name", name).firstResult();
    }
}
1 用于查找具有匹配 GitHub 账户名称的身份记录的实用方法。

其次,我们创建一个 import.sql 脚本,将演示记录添加到数据库中。

INSERT INTO identity(id, name, permission) VALUES (1, '${user.name}', 'read:name'); (1)
1 插入一条演示记录。您将在启动 MCP 服务器时提供您的 GitHub 账户名称。

最后,我们创建一个安全身份增强器。

package io.quarkiverse.langchain4j.sample;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;

@ApplicationScoped
public class SecurityIdentityPermissionAugmentor implements SecurityIdentityAugmentor { (1)

    @Inject
    HibernateBlockingAugmentor hibernateBlockingAugmentor;

    @Override
    public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
        return context.runBlocking(() -> hibernateBlockingAugmentor.augment(identity)); (2)
    }

    @ApplicationScoped
    static class HibernateBlockingAugmentor {

        @ActivateRequestContext
        public SecurityIdentity augment(SecurityIdentity securityIdentity) {
            Identity identity = Identity.findByName(securityIdentity.getPrincipal().getName()); (3)

            QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(securityIdentity); (4)
            return builder.addPermissionAsString(identity.permission).build(); (5)
        }
    }
}
1 自定义 SecurityIdentityAugmentor 可以增强已验证的安全身份。
2 以阻塞模式运行增强,因为它需要访问数据库。
3 查找与当前用户姓名匹配的已记录 Identity
4 从当前身份初始化一个安全身份构建器。
5 添加分配给该用户的权限,并创建一个更新的 SecurityIdentity

仅此而已,增强步骤仅用几行代码就完成了。

MCP 服务器配置

让我们配置我们的安全 MCP 服务器。

quarkus.mcp.server.traffic-logging.enabled=true (1)
quarkus.mcp.server.traffic-logging.text-limit=1000

quarkus.http.auth.permission.authenticated.paths=/mcp/sse (2)
quarkus.http.auth.permission.authenticated.policy=authenticated

quarkus.oidc.provider=github (3)
quarkus.oidc.application-type=service (4)

quarkus.hibernate-orm.database.generation=drop-and-create (5)
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql

quarkus.http.port=8081 (6)
1 启用 MCP 服务器流量日志记录。
2 在初始握手期间强制对主要的 MCP SSE 端点进行已身份验证的访问。另请参阅上面“MCP 服务器工具”部分中如何使用注解保护工具,尽管您也可以通过在配置中列出主端点和工具端点来保护工具的访问,例如:quarkus.http.auth.permission.authenticated.paths=/mcp/sse,/mcp/messages/*
3 要求只能使用 GitHub 访问令牌来访问 MCP 服务器。
4 默认情况下,quarkus.oidc.provider=github 只支持授权码流程。quarkus.oidc.application-type=service 会覆盖它,并要求使用 bearer 令牌。
5 存储身份记录的数据库由 PostgreSQL DevService 支持。
6 在端口 8081 上启动 MCP 服务器——这是为 Quarkus LangChain4j Poem Service 应用程序准备的,该应用程序使用 MCP 客户端来允许其在默认 8080 端口上启动。

在开发模式下启动 MCP 服务器

mvn quarkus:dev -Duser.name="Your GitHub account name" (1)
1 使用您的 GitHub 账户名称,例如,mvn quarkus:dev -Duser.name="John Doe"。这是正确将用户名和权限数据导入数据库所必需的。

MCP 服务器的与安全相关的配置在生产模式下保持完全相同,因此我们不讨论在生产模式下运行 MCP 服务器,以节省博客文章的空间。如果您想在生产模式下运行 MCP 服务器,请查看 Quarkus LangChain4j Secure MCP Client Server 示例 ——您只需要确保在生产模式下也可以使用 PostresSQL。

步骤 2 - 创建并启动使用 AI Gemini 和 MCP 客户端的 Poem Service

MCP 服务器现在正在运行,并准备好接受工具调用。让我们创建一个 AI Poem Service,它将与 AI Gemini 配合使用,并使用 MCP 客户端来完成工具调用。

Poem Service Maven 依赖项

添加以下依赖项

<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-ai-gemini</artifactId> (1)
</dependency>
<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-mcp</artifactId> (2)
</dependency>
<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-oidc-mcp-auth-provider</artifactId> (3)
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId> (4)
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-qute</artifactId> (5)
</dependency>
1 quarkus-langchain4j-ai-gemini 提供了对 AI Gemini 的支持。
2 quarkus-langchain4j-mcp 提供了核心 MCP Client 支持。
3 quarkus-langchain4j-oidc-mcp-auth-provider 提供了 McpClientAuthProvider 的实现,该实现可以提供在 GitHub OAuth2 授权码流程中获得的访问令牌。
4 quarkus-oidc 支持 GitHub OAuth2 登录以保护对 Poem Service 的访问。其版本在 Quarkus BOM 中定义。
5 quarkus-rest-qute 生成一个 HTML 页面来欢迎登录用户。其版本在 Quarkus BOM 中定义。

注册 GitHub OAuth2 应用程序

注册一个 GitHub OAuth2 应用程序,您将在登录 Poem Service 应用程序时授权该应用程序。

遵循 GitHub OAuth2 注册流程,并确保注册 https://:8080/login 回调 URL。

使用生成的 GitHub 客户端 ID 和密钥,设置 GITHUB_CLIENT_IDGITHUB_CLIENT_SECRET 环境变量,或更新 application.properties 中 quarkus.oidc.client-id=${github_client_id}quarkus.oidc.credentials.secret=${github_client_secret} 属性,将 ${github_client_id} 替换为生成的客户端 ID,将 ${github_client_secret} 替换为生成的客户端密钥。

默认情况下,Quarkus GitHub 提供程序在 HTTP Authorization 头部提交客户端 ID 和密钥。但是,GitHub 可能要求将客户端 ID 和密钥都作为表单参数提交。

如果您在登录 GitHub 并重定向回 Quarkus MCP 服务器后收到 HTTP 401 错误,请尝试用以下两个属性替换 quarkus.oidc.credentials.secret=${github.client.secret} 属性:

quarkus.oidc.credentials.client-secret.method=post
quarkus.oidc.credentials.client-secret.value=${github.client.secret}

AI Gemini API 密钥

Poem Service 依赖 AI Gemini 为登录用户创建诗歌。

获取 AI Gemini API 密钥,并设置 AI_GEMINI_API_KEY 环境变量,或在 application.properties 中更新 quarkus.langchain4j.ai.gemini.api-key=${ai_gemini_api_key} 属性,将 ${ai_gemini_api_key} 替换为 API 密钥值。

GitHub 登录端点

Poem Service 需要一个管理 GitHub OAuth2 登录的端点。通常,这样的端点会欢迎登录用户,并提供链接供用户导航到受保护应用程序的其余部分。

让我们实现这个登录端点。

package io.quarkiverse.langchain4j.sample;

import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;

/**
 * Login resource which returns a poem welcome page to the authenticated user
 */
@Path("/login")
@Authenticated (1)
public class LoginResource {

    @Inject
    UserInfo userInfo; (2)

    @Inject
    Template poem;

    @GET
    @Produces("text/html")
    public TemplateInstance poem() {
        return poem.data("name", userInfo.getName()); (3)
    }
}
1 需要已身份验证的访问。它会强制为尚未登录 GitHub 的用户执行授权码流程,并为已身份验证的用户执行会话验证。
2 GitHub 访问令牌是二进制的,Quarkus OIDC 通过使用它们来请求 GitHub 特定 UserInfo 表示来间接验证它们。
3 用户登录 GitHub 并重定向到此端点后,将使用一个简单的 Qute 模板 生成一个包含用户名和指向 Poem Resource 端点 的链接的 HTML 页面,并返回给用户。

创建 Poem Resource 端点

Poem Resource 端点接受已身份验证用户的诗歌请求,并将这些请求委托给 AI Poem Service,后者使用 AI GeminiAI Gemini 依赖 MCP 客户端来获取登录用户的姓名。

package io.quarkiverse.langchain4j.sample;

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/poem")
@Authenticated (1)
public class PoemResource {

    static final String USER_MESSAGE = """
            Write a short 1 paragraph poem about a Java programming language.
            Please start by greeting the currently logged in user by name and asking to enjoy reading the poem.""";

    @RegisterAiService
    public interface PoemService { (2)
        @UserMessage(USER_MESSAGE)
        @McpToolBox("user-name") (3)
        String writePoem();
    }

    @Inject
    PoemService poemService;

    @GET
    public String getPoem() {
        return poemService.writePoem(); (4)
    }
}
1 要求已身份验证的诗歌请求。
2 AI Poem Service 接口。
3 请参阅 MCP 客户端 user-name 配置,请参阅下面的“Poem Service 配置”部分。

Poem Service 配置

让我们看看 Poem Service 的配置。

quarkus.langchain4j.mcp.user-name.transport-type=http (1)
quarkus.langchain4j.mcp.user-name.url=https://:8081/mcp/sse/ (2)

quarkus.oidc.provider=github (3)
quarkus.oidc.client-id=${github_client_id} (4)
quarkus.oidc.credentials.secret=${github_client_secret} (4)

quarkus.langchain4j.ai.gemini.api-key=${ai_gemini_api_key} (5)
quarkus.langchain4j.ai.gemini.log-requests=true (6)
quarkus.langchain4j.ai.gemini.log-responses=true
1 启用 MCP 客户端 HTTP 传输。在此演示中,我们使用 SSE,未来将支持 Streamable HTTP
2 指向您在“在开发模式下启动 MCP 服务器”步骤中启动的 Quarkus MCP 服务器端点。
3 要求 GitHub OAuth2 登录。
4 在“注册 GitHub OAuth2 应用程序”步骤中生成的 GitHub 客户端 ID 和密钥。
5 您在“AI Gemini API 密钥”步骤中获取的 AI Gemini 密钥。
6 启用 AI Gemini 请求和响应日志记录。

请注意,MCP 客户端配置有一个 user-name 名称。您在“创建 Poem Resource 端点”步骤中使用 @McpToolBox("user-name") 注解引用了此配置。

在开发模式下启动 Poem Service

mvn quarkus:dev

所有 Poem Service 配置在生产模式下保持完全相同,因此我们不讨论在生产模式下运行它,以节省一些博客文章空间。如果您想在生产模式下运行 Poem Service,请查看 Quarkus LangChain4j Secure MCP Client Server 示例

我们准备好测试我们的 AI Poem Service 应用程序了。

步骤 3 - 测试 Poem Service

访问 https://:8080 并登录 Poem Service

Login to Poem Service

您应该会收到一条包含您姓名和指向 Poem Service 端点的链接的响应。

Poem Service Welcome Page

此时,Quarkus MCP Client 没有参与获取您的姓名,而是由 GitHub 登录端点完成的。

点击链接获取一首诗,并让 AI Gemini 为您创作一首关于 Java 的诗。

Poem Service Response

这次,Quarkus MCP Client 帮助 AI Gemini 从安全的 Quarkus MCP 服务器获取了您的姓名。

访问令牌委派注意事项

通常,GitHub 等社交提供商颁发的访问令牌并非旨在用于您的分布式应用程序架构,其中像 Poem Service 这样的服务通过 Quarkus MCP server 这样的另一个服务间接访问 GitHub API。

已通过 GitHub 登录用户的 Quarkus REST 服务可以直接访问 GitHub API。例如,Poem Service 可以利用 Quarkus LangChain4j 的出色功能将 REST 客户端标记为工具以访问 GitHub API。请参阅 如何使用 Google Calendar service 完成的

在此演示中,我们展示了 Quarkus MCP Client 的互操作能力,可以访问 MCP 服务器并使用访问令牌访问安全的 MCP 服务器。我们使用 GitHub OAuth2,因为它对大多数开发人员来说都很容易获得。

KeycloakAuth0 等提供商可以创建旨在从一个服务传播到另一个服务的访问令牌。在企业环境中,您很可能会遇到处理此类令牌的 Quarkus MCP 服务器实现。或者,在可能的情况下,接受已身份验证用户的 AI 服务应用程序可以请求令牌发行方将其访问令牌交换为另一个令牌,该令牌将用于访问下游 MCP 服务器。

Quarkus AI Service 应用程序可能需要并且可以支持委派流程,例如 GitHub 访问令牌 → Poem Service → MCP Client → MCP Server tool → GitHub API,并附加 Quarkus 团队将在未来的博客文章中讨论的附加安全措施,以及此演示中所示的身份增强。

结论

在这篇博文中,我们演示了 Quarkus MCP Client 如何通过传播 OAuth2 授权码流程完成后 Quarkus LangChain4j AI Service 应用程序可用的访问令牌来访问安全的 MCP 服务器。

敬请关注有关使用 Quarkus MCP Client 和 MCP Server 安全地使用 MCP 的更多即将发布的博客文章。

尽情享受!