Ohhnews

分类导航

$ cd ..
Spring Blog原文

使用 Spring AI 与 MCP Apps 构建交互式 AI 界面

#spring ai#mcp#人工智能#用户界面#开发指南

人类与人工智能(AI)的典型交互方式是通过类似于 ChatGPT 或 Claude Desktop 的聊天界面。事实上,能够用自然语言与 AI 对话,或许是这项技术最令人惊叹的地方之一。它让人类能够以符合人类逻辑的方式与计算机交谈,而无需局限于由按钮、列表和文本框组成的传统界面。

尽管传统的用户界面(UI)不如聊天界面那样灵活流畅,但在某些场景下,它们依然是与应用程序进行交流的最佳且最自然的方式。例如,在地图上点击某个位置,就比试图用自然语言描述具体位置要精确自然得多。(除非你恰好知道该位置的经纬度。)

如果能兼顾两者的优点会怎样?如果你可以在与 AI 聊天时,在适当的时候调用更传统的 UI 界面呢?这正是 MCP Apps 带给我们的功能:在聊天界面中嵌入丰富的 UI 元素。其结果是一种混合体验,聊天与应用程序融为一体,允许用户不仅可以提问,还能在不离开聊天界面的情况下,主动与工具和工作流进行交互。

得益于 Spring AI 社区的贡献(特别感谢 Vadzim Shurmialiou 和 Alexandros Pappas 提交的拉取请求),现在使用 Spring AI 2.0.0-M3 创建作为 MCP 服务器一部分的富 UI 界面变得非常简单。在本文中,我们将动手实现这一功能。

MCP Apps 概览

从本质上讲,一个 MCP App 包含两个主要要素:

  • 由 MCP 服务器提供的一个工具,该工具包含引用 HTML 资源的元数据。
  • 由 MCP 服务器提供的一个 HTML 资源(包含 JavaScript 和 CSS),构成了 UI 本身。

UI 本身是一种特殊的 MCP 客户端,可以使用 JSON-RPC 与 MCP 服务器及其宿主助手进行通信。为了简化服务器交互,提供了一个 ext-apps.ts 模块,UI 可以利用它来调用工具、处理上下文变更、向宿主推送消息等。

为了演示如何使用 Spring AI 构建 MCP Apps,我们来创建一个简单但有趣的掷骰子示例。

初始化项目

创建包含 MCP App 的 MCP 服务器的第一步是创建一个 MCP 服务器。使用 Spring AI,首先可以使用 Spring Boot Initializr(通过 IDE 或访问 start.spring.io)来初始化一个包含 Spring MVC 和 Model Context Protocol Server 依赖的 Spring Boot 项目。


⚠️ 重要提示

项目初始化完成后,请确保 Spring AI 版本为 2.0.0-M3 或更高。正是从该版本开始,MCP 注解被合并到了 Spring AI 中,并增加了在工具和资源上指定元数据的能力——这对于开发 MCP Apps 至关重要。

对于 Gradle 构建的项目,你应该在 build.gradle 中看到以下代码块:

$ groovy
ext {
      set('springAiVersion', "2.0.0-M3")
    }

如果你使用 Maven,请查找类似如下的 <properties> 块:

$ xml
<properties>
      <!-- 其他属性,如 Java 版本 -->
      <spring-ai.version>2.0.0-M3</spring-ai.version>
    </properties>

项目初始化完成后,就可以定义应用程序的用户界面了。

定义用户界面

MCP App 的用户界面是一个标准的 HTML 文件,外加任何配套的 JavaScript 和/或 CSS。为了演示 MCP Apps,我们创建一个掷骰子 UI。以下 HTML 定义了该界面(位于 src/main/resources/app/dice-app.html):

$ markup
<!DOCTYPE html>
<html>
<head>
    <title>Dice Roller</title>
    <style>
        body { font-family: sans-serif; text-align: center; margin-top: 50px; }
        .dice { font-size: 80px; }
        button { font-size: 18px; padding: 8px 16px; }

        @media (prefers-color-scheme: dark) {
            body {
                background-color: #1d1e22;
                color: white;
            }}
    </style>
</head>
<body>

<div class="dice">
    <span id="die1"></span>
    <span id="die2"></span>
</div>

<br>
<button id="roll-dice-btn">Roll Dice</button>

<script type="module">
    import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.2/app-with-deps";
    const app = new App({ name: "roll-the-dice", version: "1.0.0" });

    app.connect().then(() => {
        rollDice().then(() => {});
    });

    const updateContext = async (diceRoll) => {
        let message = `The dice roll results are ${diceRoll.die1} and ${diceRoll.die2}.`;
        app.updateModelContext({
            content: [{ type: "text", text: message }],
        }).then(() => {});
    };

    const diceFaces = ["⚀","⚁","⚂","⚃","⚄","⚅"];

    const rollDiceBtn = document.getElementById("roll-dice-btn");
    rollDiceBtn.addEventListener("click", async () => {
        await rollDice();
    });

    const randomIndex = () => {
        return Math.floor(Math.random() * 6);
    };

    const rollDice = async () => {
        const die1 = document.getElementById("die1");
        const die2 = document.getElementById("die2");

        let rolls = 0;
        let final1, final2;

        const animation = setInterval(() => {
            final1 = randomIndex();
            final2 = randomIndex();

            die1.textContent = diceFaces[final1];
            die2.textContent = diceFaces[final2];

            rolls++;
            if (rolls > 15) {
                let diceRoll = {
                    die1: final1 + 1,
                    die2: final2 + 1
                };

                updateContext(diceRoll).then(() => {});

                clearInterval(animation);
            }
        }, 30);
    }
</script>

</body>
</html>

HTML 代码虽然较多,但大部分用于定义 UI 的视觉元素以及 JavaScript 驱动的动画效果。其中 <script> 块的前几行对于创建 MCP Apps 至关重要。

首先是导入 ext-apps.ts 模块并创建 App 实例。App 实例用于与托管该 MCP App 的 AI 助手(如 Claude Desktop、MCP Jam 或 Goose)进行交互。

接下来是连接到助手宿主。连接成功后,它会调用 rollDice() 函数执行初始掷骰动作。

最后,updateContext() 函数利用 App 对象的 updateModelContext() 将掷骰结果发送给助手,从而更新其上下文。这会将掷骰结果注入聊天历史记录中,确保后续的聊天交互能够感知骰子的当前状态。

以下时序图有助于直观理解整个流程:

[LOADING...]

值得注意的是,HTML/JavaScript 视图还有其他与 MCP 服务器交互的方式。除了 updateModelContext(),还有两个有用的函数:sendMessage()callServerTool()sendMessage() 允许你注入一条消息,就像用户自己输入的一样;而 callServerTool() 则允许从 UI 调用 MCP 服务器上的工具。

创建 MCP 服务器

使用 Spring AI 开发 MCP 服务器,需要创建一个包含 @McpTool@McpResource 注解方法的 Bean。对于 MCP App,你需要两个方法:一个用 @McpResource 注解的方法来提供 HTML UI,一个用 @McpTool 注解的方法来关联 MCP App。

首先,定义 @McpResource 注解的方法:

$ java
@Service
public class DiceApp {

    @Value("classpath:/app/dice-app.html")
    private Resource diceAppResource;

    @McpResource(name = "Dice App Resource",
        uri = "ui://dice/dice-app.html",
        mimeType = "text/html;profile=mcp-app",
        metaProvider = CspMetaProvider.class)
    public String getDiceAppResource() throws IOException {
      return diceAppResource.getContentAsString(Charset.defaultCharset());
    }

    public static final class CspMetaProvider implements MetaProvider {
      @Override
      public Map<String, Object> getMeta() {
        return Map.of("ui",
            Map.of("csp",
                Map.of("resourceDomains",
                    List.of("https://unpkg.com"))));
      }
    }
}

DiceApp 类标注了 @Service 以便被自动扫描。由于 HTML UI 从 unpkg.com 加载 ext-apps.ts,我们需要设置内容安全策略(CSP),通过 CspMetaProviderui.csp.resourceDomains 设置为允许访问 https://unpkg.com

接下来添加工具方法:

$ java
@McpTool(
      title = "Roll the Dice",
      name = "roll-the-dice",
      description = "Rolls the dice",
      metaProvider = DiceMetaProvider.class)
    public String rollTheDice() {
      return "Opening dice roller app.";
    }

    public static final class DiceMetaProvider implements MetaProvider {
      @Override
      public Map<String, Object> getMeta() {
        return Map.of("ui",
            Map.of(
                "resourceUri", "ui://dice/dice-app.html"));
      }
    }

rollTheDice() 方法通过 metaProvider 关联了 UI 资源。最后,在 application.properties 中添加配置:

$ properties
spring.ai.mcp.server.protocol=streamable
server.port=3001

我们推荐使用 streamable HTTP 传输协议。

运行 MCP App

你可以像运行任何 Spring Boot 应用程序一样启动它:

$ bash
./gradlew bootRun

启动后,在你的 MCP 客户端(如 MCP Jam 或 Claude Desktop)中配置使用 http://localhost:3001/mcp

如果使用 Claude Desktop,由于其尚不支持 Streamable HTTP 传输,你需要使用 mcp-remote 进行代理:

$ cat
"mcpServers": {
      "dice-tools": {
        "command": "npx",
        "args": [
          "-y",
          "mcp-remote",
          "http://localhost:3001/mcp"
        ]
      }
    }

配置完成后,在聊天中输入“Roll the dice”。授予权限后,你将看到掷骰子界面出现在聊天框中。

结论

开发 MCP App 意味着构建一个包含 @McpResource(提供 UI)和 @McpTool(触发应用)的 MCP 服务器。Spring AI 通过简化元数据配置,使这一过程变得非常直观。

MCP App 打破了 LLM 仅限于文本交互的局限,为在聊天中直接实现丰富、响应式的 UI 开辟了无限可能。你打算如何利用 MCP Apps 创造更具交互性的 AI 体验呢?

资源