🙋 前言
正如标题所说 AI + MCP 渲染前端 UI
,通过 AI 结合 MCP
以一种方式渲染前端的 UI。会有同学问:"这样做有什么好处?" 大家不妨想一下,目前 AI 已经逐渐普及,对于非大模型研究员而言的程序员,大致方向都是利用 AI 创造,而不是研究大模型对吧。
身为前端程序员的我,方向也与 👆 一致,在思考我可以用 AI 创造些什么。 前端接触最多的就是web 网页
, 相信大家不难发现,越来越多的网站都接入了业务知识库的智能助手,这正是前端网页在拥抱 AI 带来的变化,AI 同时也在赋能业务的体现。
💡 灵感
知识库智能助手的能力 -> 偏业务性的回答你的问题
,更多的是它告诉你什么,而后需要你自己根据它回复的内容而后我们再去执行。例如在订票网站中,我询问 "3.15 9点从深圳到达长沙的高铁票有哪些?" 这时智能助手会输出 车次 1、 车次 2、车次 3。这时候你还需要复制这些车次去网站 UI 中进行搜索车次,而后再进行购票流程。
我在想,使用 AI 的目的是简化人为的路径操作
,上述确实减少了用户条件查询翻找的操作路径。但我觉得还不够,我发现 UI 的操作路径还能更加简化
,例如当我询问 "3.15 9点从深圳到达长沙的高铁票有哪些?",智能助手直接给我输出这三个车次,并且还附带上了购票按钮,那我就直接可以在聊天框完成购票了 nice!
再举一个直观的例子,比如快递地址的输入,当我在聊天助手中说添加一个收货地址的时候,这时聊天框会输出一个地址表单 UI 供你填入信息
。节省了点击 "我的 -> 收货地址 -> 添加收货地址" 的路径。
🤔 思考
❓ 提问
那有同学问了:缩短 UI 操作路径和 AI 结合 MCP
有什么关系呢?
AI llm 大模型通过识别自然语言获取 MCP tool description 来进行工具调用。那如果我在现有的智能助手的 llm 中接入 MCP 机制,我在每个 MCP server 中定义当符合我的 tool description 的时候我就返回某个 UI 组件到前端,然后前端进行组件渲染就可以。
好处
这样做的好处就是 UI 渲染的能力完全由我们自己决定、并且在 mcp server 后端中也可以调用业务接口,进行逻辑处理。
方向
这个时候我的步骤方向就很清晰了
- 前端发送信息给聊天助手。
- 聊天助手根据信息调用指定 mcp server tool。
- mcp server tool 返回指定结构。
- 前端接收到对应结构,前端渲染 UI 处理。
目标
期望能够以最小成本在前端现有项目中接入上述的方案。假设你的公司正在使用智能助手,也可以很轻松的在项目中接入这套方案,实现 UI 的渲染。不仅如此,UI 也伴随着 JS 的运行,同时 JS 也能根据业务需要去运行,因此这里畅想的空间很大。
🚗 浅谈实现思路
1、前端发送消息给聊天助手
这一步骤是比较常规的,比如后端提供的接口为 /message
接口,只需要前端调用接口,即可获取流式数据进行渲染。
2、聊天助手后端调用 MCP
这里稍微偏后端一些,实现原理是大模型通过 function calling 去获取指定参数,例如获取到 mcp server name、mcp server arg ,然后你再手动调用 mcp host 通过指定 mcp client 调用其对应 mcp server。 不熟悉概念的小伙伴可以看看官网介绍 modelcontextprotocol.io/docs/concep...
例如,关键点是 tools 参数
typescript
static async createChatCompletion(messages: any[], tools?: any[]) {
return await openai.chat.completions.create({
messages: messages || [
{ role: "system", content: "You are a helpful assistant." },
],
model: "deepseek-chat",
tools: tools ?? [],
});
}
这里的 mcpHost 是我实现的 mcp 客户端,后续会提到哈。当然了后端内容是可以完全定制的,这里只是举个例子。
typescript
static async getAvailableTools() {
const tools = await mcpHost.getTools();
const toolsList = tools;
const availableTools = toolsList?.reduce((pre, cur) => {
if (cur?.tools?.length) {
// @ts-ignore
cur.tools.forEach((item) => {
console.log(`${cur.server_name}_${item.name}`);
// 确保 inputSchema 有效
const inputSchema = item.inputSchema || {};
const schemaType = inputSchema.type || "object";
pre.push({
type: "function",
function: {
name: `${cur.server_name}_${item.name}`,
description: item?.description || "",
parameters: {
type: schemaType,
required: inputSchema.required || [],
properties: inputSchema.properties || {},
},
},
});
});
}
return pre;
}, [] as any[]);
return availableTools;
}
const toolCall = content.message.tool_calls[0];
const toolName = toolCall.function.name;
const toolArgs = toolCall.function.arguments;
const serverName = toolName.split("_")[0];
// 第一个 _ 后面的内容,可能存在多个 _
const functionName = toolName.split("_").slice(1).join("_");
console.log("serverName", serverName);
console.log("functionName", functionName);
console.log("toolArgs", toolArgs);
const toolResult = await MCPService.callTool(serverName, functionName, JSON.parse(toolArgs));
3、MCP server tool 返回指定结构
举个 mcp server 中的例子。可以看到当 tool 为 RecommendBook 会执行推荐逻辑。content 为展示到输出内容,_meta 是携带到前端的字段。可以看到内部包含 props, 这个 props 就是需要注入到组件的数据,为什么有这个属性?我们后面会提到。
typescript
case 'RecommendBook': {
const { title, author } = args;
let recommendBookList = []
if (!title && !author) {
recommendBookList = books.sort(() => Math.random() - 0.5).slice(0, 3)
} else if (title && !author) {
// 模糊的查找
recommendBookList = books.filter((book) => book.title.incsludes(title))
} else if (!title && author) {
recommendBookList = books.filter((book) => book.author.includes(author))
} else {
recommendBookList = books.filter((book) => book.title.includes(title) || book.author.includes(author))
}
return {
content: [
{ type: "text", text: "show book list" },
],
_meta: {
aiOutput: {
type: "text",
content: `Recommend book list is starting to render...`,
},
props: {
recommendedBooks: recommendBookList,
},
},
};
}
4. 前端接收到对应结构
前端在接收到 _meta 后如何渲染 UI 呢
可以通过动态组件的能力进行渲染。以 react 为例使用 lazy 进行渲染
typescript
lazy(() => import("path")),
总不能让用户去定义 mapping 然后做匹配吧,可以,但没必要。可以通过构建工具添加来实现这层 mapping
typescript
/**
* @mcp-comp RecommendBook
* @mcp-prop-path recommendedBooks
*/
export interface IncludeBook {
id: string;
/**
* @mcp-input-optional book title
*/
title: string;
/**
* @mcp-input-optional book author
*/
author: string;
cover: string;
price: number;
}
/**
* @mcp-comp RecommendBook
* @mcp-description recommend book for user
* @mcp-server-name mcp-component-render
*/
interface RecommendListProps {
recommendedBooks: IncludeBook[];
}
const RecommendList: React.FC<RecommendListProps> = ({ recommendedBooks }) => {
构建工具通过注释生成 mapping 关系图。
✅ 至此思路完成!
可以答疑一下上面提到的 props ,没错,就是为了注入到前端的组件中,为什么是后端生成的呢?
比如展示我的个人档案,有两种方法,前端 UI 通过 id 查找,你也需要注入 id props,一种是后端查到档案 UI 组件的 props 然后注入。
🎁 不仅于此
作者根据上面提到的实现了一整套方案。
目前文档还在完善中 ✍️
文档地址:mcpsynergy.github.io/docs/
如果你觉得对你有帮助,不妨点个 star 🌟 吧
前端
目前作者只实现了 react 框架下的前端 sdk。 vue 的后续会支持。
1、react 动态可校验组件 sdk @mcp-synergy/react
2、vite 插件生成组件 mapping 关系 @mcp-synergy/vite-plugin-comp
后端
目前只实现了 node 下的 mcp 客户端方便你像 cursor 一样管理 mcp。
1、nodejs mcp 客户端sdk 包 @mcp-synergy/host
其余的后端逻辑以各业务为主。
Demo
启动 client 、server 项目中的 demo 后可以看到我实现的 demo。 github.com/McpSynergy/...