Quarkus+模型上下文协议MCP+Langchain4j教程

在本文中,我们使用Quarkus配置了一个MCP服务器和一个连接到这个MCP服务器的单独的MCP客户端应用程序。我们使用Quarkus和LangChain4j来构建客户端应用程序。在幕后,我们将其连接到使用Ollama API连接到Mistral LLM。

我们通过为Mistral LLM提供工具来获取JVM系统信息和当前时间,从而增强了它的功能。如果没有工具,LLM就无法获得这些动态信息。我们在本文中使用的这些工具不太可能是在真实的项目中使用的工具。也就是说,他们确实证明了通过提供我们自己的定制工具来增强LLM功能的可能性。

最后,我们注意到MCP服务器允许我们使用自己的代码提供各种功能,MCP客户端允许我们轻松地将这些工具集成到LLM连接的应用程序中。

在本教程中,我们将研究使用Quarkus和LangChain 4J构建模型上下文协议服务器和客户端。我们将创建一个简单的聊天机器人,并通过MCP服务器扩展LLM的功能,以执行简单的自定义任务,例如基于时区和服务器JVM的信息获取当前日期。

模型上下文协议101
Anthropic于2024年11月开源了他们的模型上下文协议(MCP)。MCP提供了一种标准化的方式来连接AI驱动的应用程序与外部数据源。在我们开始编写任何代码之前,让我们首先了解为什么我们需要MCP以及它是什么。

为什么我们需要模型上下文协议?
随着人工智能应用变得越来越普遍,人们对通过LLM暴露组织和特定于应用程序的“上下文”的期望正在增长。因此,将这样的上下文集成到AI工作流中也需要一些考虑。

上下文可以从几个不同的数据源(如数据库、文件系统、搜索引擎和其他工具)集成到AI工作流中。由于各种各样的数据源及其底层连接方法,将这种背景引入人工智能集成构成了一个重大挑战。

这种集成可以融入AI应用程序本身,但这可能会增加此类应用程序的复杂性。此外,许多组织已经有很多现有的内部应用程序服务,提供各种各样的信息。理想情况下,我们希望能够将现有服务与AI集成,而无需紧密耦合。

模型上下文协议简介
我们现在理解需要某种协议来帮助将来自各种来源的上下文汇集在一起。模型上下文协议满足了这一需求。通过MCP,我们可以在本地LLM之上构建复杂的代理和工作流,而无需将它们紧密耦合。

这类似于用户界面如何通过使用REST API与后端通信。一旦建立了API合约,只要API合约不变,UI和后端都可以独立修改。

我们注意到,即使LLM使用大量数据进行训练,并且在其内存中存储了大量信息,但它们并不知道当前或专有信息。探索MCP的一个简单方法是尝试一些允许LLM查询此类信息的简单服务。

在我们深入研究代码之前,让我们快速了解一下MCP架构及其组件:

Architecture diagram of the model context protocol (MCP) demonstrating the relationship between host, clients, servers, and external sources.

MCP遵循客户端-服务器架构,包括几个关键组件:

  • MCP主机是我们的主要应用程序,它与LLM集成,需要与外部数据源连接
  • MCP客户端是与MCP服务器建立和维护1:1连接的组件
  • MCP服务器是与外部数据源集成并公开与它们交互的功能的组件
为了处理客户端和服务器之间的通信,MCP还提供了两个传输通道:
  1. 标准输入/输出(stdio)传输类型用于通过标准输入和输出流与本地进程和命令行工具进行通信。
  2. 用于客户端和服务器之间基于HTTP的通信的服务器发送事件(SSE)传输类型。
MCP是一个复杂而庞大的主题;我们可以参考官方文档来了解更多信息。

创建自定义MCP服务器
有几个预建的MCP服务器,我们可以使用很少的设置和配置。然而,在本文中,我们将介绍如何使用Quarkus创建自己的MCP服务器。

为此,我们将使用Quarkus构建两个简单的功能工具。一个工具根据时区获取当前日期,第二个工具提供关于服务器JVM的信息。

1.创建Quarkus MCP服务器项目
我们需要安装JDK和Quarkus CLI作为先决条件。一旦先决条件可用,我们就可以创建一个新项目:

quarkus create app --no-code -x qute,quarkus-mcp-server-sse quarkus-mcp-server

这将在名为quarkus-mcp-server的文件夹下创建一个新项目,其中包含Quarkus项目的基本脚手架。

2.依赖关系
使用quarkus CLI创建的项目会自动添加核心依赖项(quarkus-arc和quarkus-junit 5)以及基于我们在命令中指定的扩展的相关附加依赖项。在我们的例子中,这些扩展是qute和quarkus-mcp-server-sse:

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.quarkiverse.mcp</groupId>
        <artifactId>quarkus-mcp-server-sse</artifactId>
        <version>1.1.1</version>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-qute</artifactId>
    </dependency>
</dependencies>

我们现在可以继续创建LLM稍后可以使用的工具。

3.定义和公开自定义工具
我们将添加几个工具。大多数LLM在没有工具的情况下无法访问当前信息。因此,如果我们询问LLM当前的日期和时间,它很可能会回答LLM训练数据被冻结的日期和时间。

因此,为了测试通过MCP服务器增强LLM功能,我们将创建一个简单的工具,提供用户(或函数调用者)时区中的当前日期和时间:

@Tool(description = "Get the current time in a specific timezone.")
public String getTimeInTimezone(
  @ToolArg(description =
"Timezone ID (e.g., America/Los_Angeles)") String timezoneId) {
    try {
        ZoneId zoneId = ZoneId.of(timezoneId);
        ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneId);
        DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.FULL)
          .withLocale(Locale.getDefault());
        return zonedDateTime.format(formatter);
    } catch (Exception e) {
        return
"Invalid timezone ID: " + timezoneId + ". Please provide a valid IANA timezone ID.";
    }
}

我们将把这个功能工具封装在一个名为ToolBox的普通Java类中。Quarkus的@Tool(io.quarkiverse.mcp.server.Tool)注释告诉Quarkus框架,注释的方法将作为MCP服务器中的工具公开。

此外,@ToolArg(io.quarkiverse.mcp.server.ToolArg)注释告诉Quarkus框架,注释的函数参数应该与提供的描述一起公开。

这里需要注意的是,描述字段应该清楚地指定调用者正确使用函数所需的有关函数的信息。该描述有助于LLM识别工具的用途,从而选择在回答最终用户的查询时应考虑的工具。

我们将添加另一个函数来获取有关MCP服务器运行的JVM的信息:

@Tool(description = "Provides JVM system information such as available processors, free memory, total memory, and max memory.")
public String getJVMInfo() {
    StringBuilder systemInfo = new StringBuilder();
   
// Get available processors
    int availableProcessors = Runtime.getRuntime().availableProcessors();
    systemInfo.append(
"Available processors (cores): ").append(availableProcessors).append("\n");
   
// Get free memory
    long freeMemory = Runtime.getRuntime().freeMemory();
    systemInfo.append(
"Free memory (bytes): ").append(freeMemory).append("\n");
   
// Get total memory
    long totalMemory = Runtime.getRuntime().totalMemory();
    systemInfo.append(
"Total memory (bytes): ").append(totalMemory).append("\n");
   
// Get max memory
    long maxMemory = Runtime.getRuntime().maxMemory();
    systemInfo.append(
"Max memory (bytes): ").append(maxMemory).append("\n");
    return systemInfo.toString();
}

运行MCP服务器
现在我们有了几个可用的工具,我们可以在开发模式下运行Quarkus服务器来快速测试它们。

为了简化部署和开发,我们将服务器打包为uber-jar。这使得mvn可以安装并发布为Maven存储库,这使得我们和其他人更容易共享和运行。

默认情况下,Quarkus使用HTTP端口8080。因此,我们将更改9000,因为稍后我们将需要8080端口用于MCP客户端包。我们在application.properties文件中进行了这两项更改:

quarkus.package.jar.type=uber-jar
quarkus.http.port=9000

我们现在可以使用Quarkus CLI运行Quarkus开发模式。一旦开发模式服务器启动并运行,我们希望它运行在http://localhost:9000/q/dev-ui/,执行如下命令:

quarkus dev

由于我们使用@Tool注释将我们的函数公开为MCP服务器的一部分,因此dev模式会自动添加一个MCP服务器扩展供我们使用。这使我们能够使用Quarkus开发模式提供的Open API / Postman风格的工具调用接口快速测试工具。

测试工具
我们现在将使用Quarkus Dev UI中的MCP服务器扩展来运行一些快速测试。

首先,我们导航到Dev UI下的工具选项卡。我们可以点击MCP服务器扩展下的Tools链接:

它也可以在URL http://localhost:9000/q/dev-ui/io. quarkiverse. mcp. quarkus-mcp-server-sse/tools上获得。我们注意到,我们定义的两个工具都是可见的,可以使用Call操作按钮运行快速测试:

 

让我们通过提供一个特定的时区来调用getTimeInTimezone工具。工具调用者已经根据我们的描述提供了一个预填充的JSON。我们修改它以提供一些真实的值:

{
    "timezoneId": "Asia/Kolkata"
}
输入完成后,我们单击Call按钮,响应部分立即以JSON格式提供相应的输出:

{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "isError": false,
        "content": [
            {
                "text": "Sunday, May 18, 2025, 7:33:26 PM India Standard Time",
                "type": "text"
            }
        ]
    }
}
同样,我们也可以测试另一个函数。现在我们已经准备好了服务器端,让我们移动到客户端。

4.创建自定义MCP客户端
我们已经有了一个MCP服务器,准备好了一些自定义工具。现在是时候打开这些工具来与LLM聊天了。为此,我们将使用Quarkus和LangChain4j。对于LLM API,我们将使用在本地机器上运行的Ollama和Mistral作为模型。但是,我们也可以使用LangChain4j库支持的任何其他LLM。

4.1.创建Quarkus MCP客户端项目
我们可以使用quarkus来创建一个新项目:

quarkus create app --no-code -x langchain4j-mcp,langchain4j-ollama,vertx-http quarkus-mcp-client 
这将在名为quarkus-mcp-client的文件夹下创建一个新项目,其中包含Quarkus项目的基本脚手架。

4.2.依赖关系
使用quarkus CLI创建的项目会自动添加核心依赖项(quarkus-arc和quarkus-junit 5)以及基于我们在命令中指定的扩展的相关附加依赖项。在我们的例子中,这些扩展是langchain 4j-mcp、langchain 4j-ollama和vertx-http:


    io.quarkiverse.langchain4j
    quarkus-langchain4j-mcp
    1.0.0.CR2


    io.quarkiverse.langchain4j
    quarkus-langchain4j-ollama
    1.0.0.CR2


    io.quarkus
    quarkus-vertx-http

4.3.定义聊天机器人
我们将利用Quarkus和LangChain4j来定义一个带有自定义系统提示符的聊天服务。

我们通过使用@RegisterAiService注释来创建一个名为McpClientAI的接口。Quarkus使用此注释使用LangChain 4j和配置的LLM自动生成LLM客户端服务:

@RegisterAiService
@SessionScoped
public interface McpClientAI {
    @SystemMessage("""
            You are a knowledgeable and helpful assistant powered by Mistral.
            You can answer user questions and provide clear, concise, and accurate information.
            You also have access to a set of tools via an MCP server.
            When using a tool, always convert the tool's response into a natural, human-readable answer.
            If the user's question is unclear, politely ask for clarification.
            If the question does not require tool usage, answer it directly using your own knowledge.
            Always communicate in a friendly and professional manner, and ensure your responses are easy to understand.
            """
    )
    @McpToolBox("default")
    String chat(@UserMessage String question);
}
此外,我们使用@SystemMessage注释将自定义系统提示符附加到chat()方法。这个系统提示向LLM建议它可以通过MCP服务器访问工具,并且应该使用它们来响应用户查询。

我们告诉Quarkus,MCP服务器使用@McpToolBox注释配置为名称“default”。Quarkus应将其与McpClientAI服务一起使用。

现在,我们需要告诉Quarkus在哪里可以找到LLM和使用AI服务的工具。

4.4.连接LLM和工具
Quarkus提供了许多配置选项来自动配置LLM客户端以及MCP客户端。我们需要在Quarkus的application.properties文件中设置一些属性,以连接我们的LLM和MCP服务器。

首先,我们为所有对LLM的调用设置超时。这是一个可选设置,但如果在较慢的计算机(如开发人员桌面)上运行LLM,则可能需要配置为更大的值:

quarkus.langchain4j.timeout=10s
对于我们的测试,我们将在本地机器上的端口11434上运行Ollama服务器,并将Mistral模型加载到Ollama服务器上。我们通过适当的属性通知Quarkus:

quarkus.langchain4j.chat-model.provider=ollama
quarkus.langchain4j.ollama.chat-model.model-id=mistral
quarkus.langchain4j.ollama.base-url=http://localhost:11434
接下来,我们将Quarkus指向我们之前创建的MCP服务器。根据我们的配置,此服务器在端口9000上运行。此外,我们注意到Quarkus MCP服务器在URI /mcp/sse处自动配置:

quarkus.langchain4j.mcp.default.transport-type=http
quarkus.langchain4j.mcp.default.url=http://localhost:9000/mcp/sse
通过此配置,Quarkus会自动建立与MCP服务器的连接,并使工具可供我们的聊天服务使用。属性名包含单词“default”,它应该与我们在@McpToolBox注释参数中配置的名称相匹配。我们现在可以使用Quarkus CLI运行Quarkus开发模式。一旦开发模式服务器启动并运行,我们希望它运行在http://localhost:8080/q/dev-ui/:

quarkus dev
由于我们使用了@RegisterAiService注释来创建聊天服务,因此dev模式会自动添加一个名为LangChain 4j core的扩展供我们使用。这使我们能够快速测试聊天客户端,而无需编写额外的服务。LangChain 4j代码扩展提供了一个随时可用的聊天UI,可以连接到我们的聊天服务。