使用Quarkus构建Keycloak MCP服务器的实战指南

本文详细介绍了如何使用Quarkus框架构建基于Model Context Protocol的Keycloak管理服务器,包含用户服务的CRUD操作实现、MCP工具定义以及与AI代理的集成演示。

A Keycloak示例 - 使用Quarkus构建我的第一个MCP服务器工具

最近我写了一篇关于"在Java生态系统中采用模型上下文协议"的文章。现在也是时候开始尝试自己编写MCP服务器了(也许不是第一次)。

我当然不想错过社区展示的所有酷炫功能。我的目标是学习,并可能创建一个更实用的示例。在这篇文章中,我将选择Keycloak,并为keycloak编写一个实验性的MCP服务器实现。这篇文章也是为了激发对这个主题的兴趣。为Keycloak拥有一个MCP服务器会有用吗?

什么是模型上下文协议?

模型上下文协议是Anthropic在2024年11月引入的标准。MCP的目的是拥有一个标准,帮助社区编写和使用工具、提示和资源。想象一下,你开始为Slack这样的工具编写工具,我也开始为Slack编写工具。看啊,我们都有自己的实现,但随后Slack也推出了自己的工具。现在我们有一个小问题,一是没有标准的方式与这些工具通信,二是如果Slack或GitHub拥有为其服务创建和暴露工具的部分,会使你我的生活更轻松。这正是我认为MCP非常有用的用例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
sequenceDiagram
autonumber
participant User
participant LLM
participant Client
participant MCPServer
User->>LLM: 您的问题(带有可用工具信息)
activate LLM
LLM->>LLM: 分析问题并决定使用哪些工具
   LLM->>Client: 指示使用选定的工具
deactivate LLM
activate Client
Client->>MCPServer: 执行选定的工具
activate MCPServer
MCPServer-->>Client: 工具结果
deactivate MCPServer
Client-->>LLM: 转发的工具结果
deactivate Client
activate LLM
LLM->>LLM: 使用工具结果制定答案
LLM-->>User: 最终响应!
deactivate LLM

解释

  1. 用户向LLM发送查询/问题
  2. LLM分析问题并决定是否需要调用工具
  3. LLM然后指示客户端执行工具
  4. 客户端在MCP服务器上执行工具
  5. 客户端然后将结果返回给LLM
  6. LLM为用户制定结果

虽然这只是一个基本示例。现实是MCP还支持提示和资源。同样重要的是要说明,MCP在通用术语中并没有真正带来新功能,而是专注于标准。自从它出现以来,我们有了多个MCP服务器和框架中的实现可供使用。如Quarkus、Spring AI、MCP SDK等。

MCP使我们能够通过提供例如预构建集成的选择和在不同LLM之间切换的灵活性来开发代理和复杂的工作流。

Stdio与SSE

在使用标准IO(服务器和客户端在同一台机器上)或构建CLI应用程序的本地开发与使用HTTP上的服务器发送事件(SSE)(允许服务器部署在其他地方并通过API访问)的远程开发之间应做出关键区分。后者应被强调为现实世界多应用程序使用中最实用的。另一件重要的事情要注意的是,MCP服务器通过JSON-RPC进行通信。

JSON-RPC是一种无状态、轻量级的远程过程调用(RPC)协议。主要地,此规范定义了几种数据结构及其处理规则。它是传输无关的,因为概念可以在同一进程内、通过套接字、通过http或在许多各种消息传递环境中使用。

1
2
3
4
5
6
7
8
9
graph LR
subgraph "逻辑分解"
direction LR
JA[Java应用]
MCPC[MCP客户端]
MCPS[MCP服务器]
end
JA --> MCPC -->|Stdio| MCPS
JA --> MCPC -->|SSE| MCPS

Keycloak

如果您不熟悉Keycloak;它是一个开源身份和访问管理软件。当前版本是26,已经在野外大量使用。它提供与OAuth/OIDC、AD、LDAP和SAML v2的单点登录能力。如果您不太熟悉Keycloak,我还写了一个小的自定进度的Keycloak教程,涵盖了所有基础和一些高级配置。

让我们开始吧。从quarkus cli或通过code.quarkus.io创建一个项目。

在我的示例中,我使用stdio。这意味着一个基于CLI的标准输入输出扩展。

在pom.xml中添加stdio Quarkus扩展

1
2
3
4
5
<dependency>
    <groupId>io.quarkiverse.mcp</groupId>
    <artifactId>quarkus-mcp-server-stdio</artifactId>
    <version>1.0.0.Alpha5</version>
</dependency>

很好。一个有趣的事实是Keycloak也是使用Quarkus构建的。最初它基于Wildfly,然而大约2年前团队将整个东西迁移到了Quarkus。Keycloak管理CLI,顾名思义,是管理Keycloak的一个伟大工具,使用REST API。我将为此项目使用它。让我们也将其添加到pom.xml中。

1
2
3
4
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-keycloak-admin-rest-client</artifactId>
</dependency>

好的,那应该为开始设置基础。

由于我是从头开始这个的。是时候编写第一个服务了。我将编写一个UserService,它将通过Keycloak管理客户端为领域中的用户调用CRUD操作。

什么是领域

领域是给定组或应用程序或服务的所有配置、选项的逻辑命名空间。一个领域保护和管理一组用户、应用程序和注册的身份代理、客户端等的安全元数据。用户可以在管理控制台内的特定领域中创建。角色(权限类型)可以在领域级别定义,您还可以设置用户角色映射以将这些权限分配给特定用户。用户属于并登录到一个领域。领域彼此隔离,只能管理和认证它们控制的用户。

1
2
3
4
5
6
7
graph TD
subgraph Realm
U[用户]
G[组]
R[角色]
C[客户端]
end

Keycloak的UserService

让我们开始编写一个服务类来访问Keycloak。

1
2
3
4
5
6
7
@ApplicationScoped // [1]
public class UserService {
    
    @Inject 
    Keycloak keycloak; // [2]

}

服务在应用程序启动时启动@ApplicationScoped。我还注入import org.keycloak.admin.client.Keycloak客户端以调用Keycloak上的管理API。

以下getUsers方法接受一个领域作为输入。这意味着我强制用户指定一个领域。因为在keycloak安装中可以有多个领域。一旦接收到领域参数,我就可以调用user().list()来获取领域中的所有用户。

1
2
3
public List<UserRepresentation> getUsers(String realm) {
    return keycloak.realm(realm).users().list();
}

对于addUser方法,我需要用户创建参数和领域。这样当进行工具调用时,我想确保上下文是领域。例如,用户可能要求从两个领域获取用户,然后将用户添加到其中一个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public String addUser(String realm, String username, String firstName, String lastName, String email, String password) {
    UserRepresentation user = new UserRepresentation(); // 设置用户字段 [1]
    user.setFirstName(firstName);
    user.setLastName(lastName);
    user.setUsername(username);
    user.setEnabled(true);
    user.setEmail(email);

    CredentialRepresentation credential = new CredentialRepresentation(); // 添加密码 [2]
    credential.setType(CredentialRepresentation.PASSWORD);
    credential.setValue(password);
    credential.setTemporary(false);
    user.setCredentials(List.of(credential));
    
    Response response = keycloak.realm(realm).users().create(user); // 发送创建请求 [3]
    if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
        return "Successfully created user: " + username;
    } else {
        Log.error("Failed to create user. Status: " + response.getStatus());
        response.close();
        return "Error creating user: "+" "+username;
    }
}

1 - UserRepresentation类是管理REST API的一部分,用于在Keycloak领域内管理用户的上下文中表示用户。此类封装了与用户相关的各种属性和属性,允许管理员以编程方式创建、更新和检索用户信息

2 - CredentialRepresentation类用于表示与用户帐户关联的凭据。此类是Keycloak管理REST API的一部分,对于管理用户认证方法(如密码、OTP(一次性密码)和其他凭据类型)至关重要

3 - 最后我向Keycloak发送创建用户请求。此部分下有更多错误处理,以确保当调用工具时,我可以返回正确的状态。

类似地,我还实现了以下两个函数,以删除用户和按名称获取用户。

1
2
public String deleteUser(String realm, String username)
public UserRepresentation getUserByUsername(String realm, String username)

UserService的完整代码列表可在github上找到

为了使上述内容成功运行,我还需要做两件操作性的东西。

在属性文件中添加以下行。这意味着我在端口8081上运行我们的本地keycloak实例。

1
quarkus.keycloak.admin-client.server-url=http://localhost:8081

通过docker-compose的Keycloak开发模式

还有一个用于keycloak的docker-compose.yaml文件,将在本地启动它。它所做的一切就是在开发模式下启动keycloak,并暴露端口8081。目前我正在用podman运行此文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    container_name: keycloak
    environment:
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=admin
    ports:
      - "8081:8080"
    command: >
      start-dev
    volumes:
      - keycloak_data:/opt/keycloak/data
    restart: unless-stopped

volumes:
  keycloak_data:

运行上述文件docker-compose up

创建UserTool

工具是一个组件,通过使LLM能够执行特定操作并与外部系统交互来增强其能力。这可能是API、数据库、内部系统等。这正是我在这里要做的,为keycloak构建工具。我们创建以下流程,其中UserTool将调用UserService,后者又调用Keycloak进行操作。我已经创建了UserService。现在是时候创建UserTool了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sequenceDiagram
autonumber
%% 定义参与者
participant UserTool
participant UserService
participant Keycloak
%% 交互序列
UserTool->>UserService: 调用UserService(例如,用户/领域/客户端)
activate UserService
UserService->>Keycloak: 调用Keycloak(获取用户/领域/客户端)
activate Keycloak
Keycloak-->>UserService: 返回结果(UserRepresentation, RealmRepresentation, ClientRepresentation)
deactivate Keycloak
UserService-->>UserTool: 从UserService返回转换结果为String
deactivate UserService

在以下UserTool类中,我注入UserService和ObjectMapper。我使用com.fasterxml.jackson.databind.ObjectMapper将一些结果转换为String,例如List。

1
2
3
4
5
6
7
8
public class UserTool {

    @Inject
    UserService userService;

    @Inject
    ObjectMapper mapper;
}

接下来,让我们创建一个MCP服务器可以暴露的工具

1
2
3
4
5
6
7
8
@Tool(description = "从keycloak领域获取所有用户") // [1]
String getUsers(@ToolArg(description = "一个表示用户所在领域名称的字符串") String realm) { // [2]
    try {
        return mapper.writeValueAsString(userService.getUsers(realm)); // [3]
    } catch (Exception e) {
        throw new ToolCallException("从领域获取用户失败");
    }
}

1 - @Tool(description = “从keycloak领域获取所有用户”) @Tool:表示getUsers方法被LLM识别为可调用函数或工具。 描述描述了这个工具的作用,例如获取所有用户。当请求从keycloak获取所有用户时,LLM可以调用此工具。 由于LLM可以理解自然语言,所有导致从keycloak获取用户上下文的问题应该*理论上调用此工具。还要注意,如果我添加太多与其他工具描述混淆的细节,LLM可能不会调用所需的工具并最终产生幻觉。应注意如何编写描述。我建议简洁直接,避免歧义和重叠。

2 - ToolArg向LLM指定此工具需要一个参数。在我的情况下,它必须是一个领域。所以如果用户只是说获取所有用户。LLM应该回来问哪个领域。如上所述,注意描述参数。

3 - 最后一旦结果从UserService返回,在这种情况下是一个List。我使用ObjectMapper将其转换为String,以便LLM可以理解响应。我也尝试过Jsonb,它工作得还不错。

1
2
3
4
5
6
7
8
9
@Tool(description = "在keycloak领域中创建新用户,具有以下必填字段:领域、用户名、firstName、lastName、email、password")  // [1]
String addUser(@ToolArg(description = "一个表示用户所在领域名称的字符串") String realm,  // [2]
               @ToolArg(description = "一个表示要创建用户的用户名的字符串") String username,
               @ToolArg(description = "一个表示要创建用户的名字的字符串") String firstName,
               @ToolArg(description = "一个表示要创建用户的姓氏的字符串") String lastName,
               @ToolArg(description = "一个表示要创建用户的电子邮件的字符串") String email,
               @ToolArg(description = "一个表示要创建用户密码的字符串") String password) {
    return userService.addUser(realm, username, firstName, lastName, email, password);  // [3]
}

1 - 由于这是创建新用户的方法。我指定了成功执行此工具所需的确切内容。

2 - 成功执行此工具所需的所有不同参数。

3 - 最后一旦UserService返回,我将结果传递给LLM进行进一步处理。

打包

我们应该将以下属性添加到application.properties

1
2
3
quarkus.package.jar.type=uber-jar # [1]
quarkus.log.file.enable=true # [2]
quarkus.log.file.path=kcadmin-quarkus.log # [3]

1 - 告诉Quarkus创建一个uber jar。Uberjar,也称为"fat jar"或"shadow jar",是一种Java归档(JAR)文件类型,包括运行Java应用程序所需的所有依赖项和资源。

2 - 我还想要一个日志文件,以便我可以了解发生了什么。

3 - 日志文件的地址和名称。

1
mvn clean package

应用程序的完整源代码可以在这里找到。如果您只想运行它:)

使用Goose运行

一个本地、可扩展、开源的AI代理,自动化工程任务 Goose by block提供了一个cli工具,具有添加MCP作为扩展的能力。使用goose configure添加LLM配置和API密钥。

一旦cli配置好,我现在可以通过将其添加为扩展来添加打包的MCP服务器。

1
goose session --with-extension="java -jar target/keycloak-mcp-server-1.0.0-SNAPSHOT-runner.jar" 

以下是一些可以问的问题示例?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
( O)> 我可以在keycloak中创建新用户吗?
是的,您可以在Keycloak中创建新用户。为此,您需要提供以下关于用户的信息:

- **领域**:用户将所在的领域名称。
- **用户名**:新用户的用户名。
- **名字**:用户的名字。
- **姓氏**:用户的姓氏。
- **电子邮件**:用户的电子邮件地址。
- **密码**:用户帐户的密码。

您可以提供这些详细信息,我可以协助您创建用户。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
( O)> 列出所有领域中的所有用户
以下是"quarkus"领域中的用户:

1. **admin**
   - ID: `af134cab-f41c-4675-b141-205f975db679`

2. **alice**
   - ID: `eb4123a3-b722-4798-9af5-8957f823657a`

3. **jdoe**
   - ID: `1eed6a8e-a853-4597-b4c6-c4c2533546a0`

1
( O)> 你能从quarkus领域删除用户sshaaf吗

好的,那就总结一下。希望您喜欢它,并准备好编写您的第一个MCP服务器实现。

总结

本文探讨了为Keycloak创建一个实用的模型上下文协议(MCP)服务器,旨在学习并展示其在AI驱动管理方面的潜力。MCP标准化了LLM如何与外部工具、提示和资源交互,解决了分散的自定义集成问题。文章详细介绍了使用Quarkus和Keycloak管理REST客户端构建这个实验性Keycloak MCP服务器的过程,专注于指定领域内的用户管理操作。它提供了UserService和MCP UserTool的代码片段,解释了如何为LLM通过Stdio消费定义工具及其参数。最后,文章展示了如何打包Quarkus应用程序并使用"Goose"(一个AI代理CLI)运行它,以使用自然语言查询与Keycloak交互。示例的源代码可以在这里找到。

资源

Goose - https://github.com/block/goose MCP - https://modelcontextprotocol.io/docs/concepts/tools Keycloak MCP服务器 - 示例 https://github.com/sshaaf/keycloak-mcp-server MCP和调用您的REST API - https://github.com/learnj-ai/llm-jakarta/tree/workshop/step-09-mcp Quarkus - https://quarkus.io/blog/mcp-server/ 使用Quarkus创建MCP服务器 - https://iocanel.com/2025/03/creating-an-mcp-server-with-quarkus-and-backstage/

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计