前端搭建 MCP Client(Web版)+ Server + Agent 实践
关注更多AI编程资讯请去AI Coding专区:https://juejin.cn/aicoding
先上个效果图,上图是在 web 版Client中使用todoist-mcp-server帮我新建了一条todolist。
本文主要介绍整体的思路和实现以及踩坑记录。
前言?MCP(Model Context Protocol)是一种开放协议,旨在通过标准化接口实现大语言模型(LLMs)与外部数据源及工具的无缝集成。MCP由 Anthropic 公司在2024年底推出,其设计理念类似于USB接口,为AI模型提供了一个“即插即用”的扩展能力,使其能够轻松连接至不同的工具和数据源?。想深入了解可查看官方文档,这里只做实战经验分享。
概念介绍?MCP Hosts(MCP 应用):如Claude Desktop、IDE、AI应用等,希望通过MCP访问数据或工具。MCP Clients(MCP 客户端):与一对一与服务端进行连接,相当于我们应用中实现数据库交互需要实现的一个客户端。MCP Servers(MCP 服务端):基于MCP协议实现特定功能的程序。Local Data Sources:本地数据源,公MCP服务器进行本地访问。Remote Services:远端服务,供MCP服务器访问远端访问,例如api的方式。?本文主要搭建Web 版本的 MCP Client和MCP Server。
技术栈系统要求:Node.js = 18(本地用了v20)
核心依赖库:CopilotKit、LangChain及其生态。
CopilotKit:React UI + 适用于 AI Copilot、AI 聊天机器人和应用内 AI 代理的优雅基础架构。LangChain.js和LangGraph:LangChain相关主要用于开发agent。langchainjs-mcp-adapters:提供了一个轻量级的包装器,使得 MCP 与 LangChain.js 兼容。modelcontextprotocol/typescript-sdk:MCP TypeScript SDKopen-mcp-client:CopilotKit 开源的 MCP Client。mcp-server-supos:一个可用的 MCP Server。Client页面大概这样,包括:左侧管理MCP Server、右侧聊天机器人
技术方案?声明:此 Client 是基于CopilotKit 开源的 MCP Clientopen-mcp-client二次改造
该代码库主要分为两个部分:
/agent– 连接到 MCP Server并调用其工具的LangGraph代理(Python)。/app– 使用 CopilotKit 进行UI和状态同步的前端应用程序(Next.js)。由于Python的agent在Windows环境下运行时报错:
本人Python编码能力有限,基于此改造成了基于JS的agent(/agent-js部分),后续均以agent-js为例;想用Python的也可按后续的改动点对/agent进行修改。
一、agent部分文件结构
核心代码「agent.js」- 基于langgraph创建workflow,其中主要节点为chat_node,该节点功能点:
定义LLMimport{ ChatOpenAI }from"@langchain/openai";
// import { HttpsProxyAgent } from "https-proxy-agent";
// const agentProxy = new HttpsProxyAgent("http://127.0.0.1:xxxx");
...
// 1 Define the model
constmodel =newChatOpenAI(
{
temperature:0,
model:"gpt-4o",
},
// todo: test, 走本地代理便于翻墙
// {
// httpAgent: agentProxy,
// }
...
?「注意:本地联调需访问 openai 时,如果是使用的代理工具,还是需要在代码里指定代理地址(HttpsProxyAgent)」
?从state获取MCP Server Configs,创建MCP Client连接到MCP Server,连通后获取Server的tools。(@langchain/mcp-adapters)constmcpConfig: any = state.mcp_config || {};
// 重要:设置环境变量时,最好把当前进程的环境变量也传递过去,确保执行Server的子进程需要的环境变量都存在
letnewMcpConfig: any = {};
Object.keys(mcpConfig).forEach((key) ={
newMcpConfig[key] = { ...mcpConfig[key] };
if(newMcpConfig[key].env) {
newMcpConfig[key].env = { ...process.env, ...newMcpConfig[key].env };
}
console.log("****mcpConfig****", mcpConfig);
// 2 Create client
constclient =newMultiServerMCPClient(newMcpConfig);
// examples
// const client = new MultiServerMCPClient({
// math: {
// transport: "stdio",
// command: "npx",
// args: ["-y", "mcp-server-supos"],
// env: {"SUPOS_API_KEY": "xxxxx"}
// },
// });
// 3 Initialize connection to the server
awaitclient.initializeConnections();
consttools = client.getTools();
基于model和tools创建代理,并调用模型发送状态中的消息// 4 Create the React agent width model and tools
constagent = createReactAgent({
llm: model,
tools,
// 5 Invoke the model with the system message and the messages in the state
constresponse =awaitagent.invoke({messages: state.messages });
「完整代码」
agent.js
/**
* This is the main entry point for the agent.
* It defines the workflow graph, state, tools, nodes and edges.
*/
import{ RunnableConfig }from"@langchain/core/runnables";
import{
MemorySaver,
START,
StateGraph,
Command,
END,
}from"@langchain/langgraph";
import{ createReactAgent }from"@langchain/langgraph/prebuilt";
import{ Connection, MultiServerMCPClient }from"@langchain/mcp-adapters";
import{ AgentState, AgentStateAnnotation }from"./state";
import{ getModel }from"./model";
// 判断操作系统
constisWindows = process.platform ==="win32";
constDEFAULT_MCP_CONFIG: Recordstring, Connection = {
supos: {
command: isWindows ?"npx.cmd":"npx",
args: [
"-y",
"mcp-server-supos",
],
env: {
SUPOS_API_URL: process.env.SUPOS_API_URL ||"",
SUPOS_API_KEY: process.env.SUPOS_API_KEY ||"",
SUPOS_MQTT_URL: process.env.SUPOS_MQTT_URL ||"",
},
transport:"stdio",
},
};
asyncfunctionchat_node(state: AgentState, config: RunnableConfig){
// 1 Define the model
constmodel = getModel(state);
constmcpConfig: any = { ...DEFAULT_MCP_CONFIG, ...(state.mcp_config || {}) };
// 重要:设置环境变量时,最好把当前进程的环境变量也传递过去,确保执行Server的子进程需要的环境变量都存在
letnewMcpConfig: any = {};
Object.keys(mcpConfig).forEach((key) ={
newMcpConfig[key] = { ...mcpConfig[key] };
if(newMcpConfig[key].env) {
newMcpConfig[key].env = { ...process.env, ...newMcpConfig[key].env };
}
console.log("****mcpConfig****", mcpConfig);
// 2 Create client
constclient =newMultiServerMCPClient(newMcpConfig);
// const client = new MultiServerMCPClient({
// math: {
// transport: "stdio",
// command: "npx",
// args: ["-y", "mcp-server-supos"],
// env: {"SUPOS_API_KEY": "xxxxx"}
// },
// });
// 3 Initialize connection to the server
awaitclient.initializeConnections();
consttools = client.getTools();
// 4 Create the React agent width model and tools
constagent = createReactAgent({
llm: model,
tools,
// 5 Invoke the model with the system message and the messages in the state
constresponse =awaitagent.invoke({messages: state.messages });
// 6 Return the response, which will be added to the state
return[
newCommand({
goto: END,
update: {messages: response.messages },
}),
}
// Define the workflow graph
constworkflow =newStateGraph(AgentStateAnnotation)
.addNode("chat_node", chat_node)
.addEdge(START,"chat_node");
constmemory =newMemorySaver();
exportconstgraph = workflow.compile({
checkpointer: memory,
});
model.js
/**
* This module provides a function to get a model based on the configuration.
*/
import{ BaseChatModel }from"@langchain/core/language_models/chat_models";
import{ AgentState }from"./state";
import{ ChatOpenAI }from"@langchain/openai";
import{ ChatAnthropic }from"@langchain/anthropic";
import{ ChatMistralAI }from"@langchain/mistralai";
// import { HttpsProxyAgent } from "https-proxy-agent";
// todo test agentProxy
// const agentProxy = new HttpsProxyAgent("http://127.0.0.1:7897");
functiongetModel(state: AgentState):BaseChatModel{
/**
* Get a model based on the environment variable.
*/
conststateModel = state.model;
conststateModelSdk = state.modelSdk;
// 解密
conststateApiKey = atob(state.apiKey ||"");
constmodel = process.env.MODEL || stateModel;
console.log(
`Using stateModelSdk:${stateModelSdk}, stateApiKey:${stateApiKey}, stateModel:${stateModel}`
if(stateModelSdk ==="openai") {
returnnewChatOpenAI({
temperature:0,
model: model ||"gpt-4o",
apiKey: stateApiKey ||undefined,
}
// {
// httpAgent: agentProxy,
// }
}
if(stateModelSdk ==="anthropic") {
returnnewChatAnthropic({
temperature:0,
modelName: model ||"claude-3-7-sonnet-latest",
apiKey: stateApiKey ||undefined,
}
if(stateModelSdk ==="mistralai") {
returnnewChatMistralAI({
temperature:0,
modelName: model ||"codestral-latest",
apiKey: stateApiKey ||undefined,
}
thrownewError("Invalid model specified");
}
export{ getModel };
state.js
import{ Annotation }from"@langchain/langgraph";
import{ CopilotKitStateAnnotation }from"@copilotkit/sdk-js/langgraph";
import{ Connection }from"@langchain/mcp-adapters";
// Define the AgentState annotation, extending MessagesState
exportconstAgentStateAnnotation = Annotation.Root({
model: Annotationstring,
modelSdk: Annotationstring,
apiKey: Annotationstring,
mcp_config: AnnotationConnection,
...CopilotKitStateAnnotation.spec,
});
exporttype AgentState =typeofAgentStateAnnotation.State;
构建和运行定义langgraph.json配置文件,定义agent相关配置,比如agent名称:sample_agent等{
"node_version":"20",
"dockerfile_lines": [],
"dependencies": ["."],
"graphs": {
"sample_agent":"./src/agent.ts:graph"// 定义agent名称等,用于前端指定使用
},
"env":".env"// 指定环境变量从.env文件中获取,生产环境可以删除该配置,从系统变量中获取
}
在本地运行时,在根路径/agent-js添加.env文件
LANGSMITH_API_KEY=lsv2_...OPENAI_API_KEY=sk-...2.借助命令行工具@langchain/langgraph-cli进行构建和运行,在package.json中定义脚本:"scripts": {
"start":"npx @langchain/langgraph-cli dev --host localhost --port 8123",
"dev":"npx @langchain/langgraph-cli dev --host localhost --port 8123 --no-browser"
},
?加上--no-browser不会自动打开本地调试的studio页面
?运行后可以在Studio预览联调等
(Studio:https://smith.langchain.com/studio/thread?baseUrl=http%3A%2F%2Flocalhost%3A8123)
注意点(踩坑记录)1. 引入modelcontextprotocol/typescript-sdk报错:?@modelcontextprotocol/sdk fails in CommonJS projects due to incompatible ESM-only dependency (pkce-challenge)
主要是modelcontextprotocol/
typescript-sdk的cjs包里面引用的pkce-challenge不支持cjs
官方的issues也有提出些解决方案,但目前为止官方还未发布解决了该问题的版本
?「解决:package.json添加"type": "module"字段,声明项目使用「ES Modules (ESM)」 规范」
?2. 配置MCP Server环境变量env问题?例如:Node.js 的child_process.spawn()方法无法找到例如npx等可执行文件。
「环境变量PATH缺失」,系统未正确识别npx的安装路径。
?可能的原因:
1)MCP Server配置了env参数后,导致传入的env覆盖了默认从父进程获取的环境变量?「解决:对配置了env的Server,将当前的环境变量合并传入」
constmcpConfig: any = state.mcp_config || {};
// 重要:设置环境变量时,最好把当前进程的环境变量也传递过去,确保执行Server的子进程需要的环境变量都存在
letnewMcpConfig: any = {};
Object.keys(mcpConfig).forEach((key) ={
newMcpConfig[key] = { ...mcpConfig[key] };
if(newMcpConfig[key].env) {
newMcpConfig[key].env = { ...process.env, ...newMcpConfig[key].env };
}
2)「跨平台路径问题」:比如在 Windows 中直接调用npx需使用npx.cmd// 判断操作系统
constisWindows = process.platform ==="win32";
constDEFAULT_MCP_CONFIG: Recordstring, Connection = {
supos: {
command: isWindows ?"npx.cmd":"npx",
args: [
"-y",
"mcp-server-supos",
],
env: {
SUPOS_API_URL: process.env.SUPOS_API_URL ||"",
SUPOS_API_KEY: process.env.SUPOS_API_KEY ||"",
SUPOS_MQTT_URL: process.env.SUPOS_MQTT_URL ||"",
},
transport:"stdio",
},
};
二、前端应用部分前端应用部分改动主要是页面上的一些功能添加等,例如支持选模型,支持配置env参数等,页面功能相关的内容就略过,可以直接看open-mcp-client,这里简单介绍下整体的一个架构。
架构方案主要是CopilotKit+Next.js,先看下「CopilotKit」官方的一个架构图:
根据本文实际用到的简化下(本文采用的CoAgents模式)
核心代码(以Next.js为例)?核心依赖@copilotkit/react-ui@copilotkit/react-core@copilotkit/runtime
1. 设置运行时端点/app/api/copilotkit/route.ts:设置agent远程端点
import{
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
langGraphPlatformEndpoint
}from"@copilotkit/runtime";;
import{ NextRequest }from"next/server";
// You can use any service adapter here for multi-agent support.
constserviceAdapter =newExperimentalEmptyAdapter();
construntime =newCopilotRuntime({
remoteEndpoints: [
langGraphPlatformEndpoint({
// agent部署地址
deploymentUrl:`${process.env.AGENT_DEPLOYMENT_URL ||'http://localhost:8123'}`,
langsmithApiKey: process.env.LANGSMITH_API_KEY,
agents: [
{
name:'sample_agent',// agent 名称
description:'A helpful LLM agent.',
}
]
}),
],
});
exportconstPOST =async(req: NextRequest) = {
const{ handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint:"/api/copilotkit",
returnhandleRequest(req);
};
2. 页面接入 CopilotKit UI/app/layout.tsx:页面最外层用CopilotKit包裹,配置runtimeUrl和agent
importtype { Metadata }from"next";
import{ Geist, Geist_Mono }from"next/font/google";
import"./globals.css";
import"@copilotkit/react-ui/styles.css";
import{ CopilotKit }from"@copilotkit/react-core";
constgeistSans = Geist({
variable:"--font-geist-sans",
subsets: ["latin"],
});
constgeistMono = Geist_Mono({
variable:"--font-geist-mono",
subsets: ["latin"],
});
exportconstmetadata: Metadata = {
title:"Open MCP Client",
description:"An open source MCP client built with CopilotKit ??",
};
exportdefaultfunctionRootLayout({
children,
}: Readonly{
children: React.ReactNode;
}){
return(
htmllang="en"
body
className={`${geistSans.variable} ${geistMono.variable}antialiasedw-screenh-screen`}
CopilotKit
runtimeUrl="/api/copilotkit"
agent="sample_agent"
showDevConsole={false}
{children}
/CopilotKit
/body
/html
}
/app/page.tsx:选择需要的聊天组件,例如CopilotPopup
"use client";
import{ CopilotPopup }from"@copilotkit/react-ui";
exportfunctionHome(){
return(
YourMainContent/
CopilotChat
className="h-full flex flex-col"
instructions={
"Youareassistingtheuserasbestasyoucan.Answerinthebestwaypossiblegiventhedatayouhave."
}
labels={{
title:"MCPAssistant",
initial:"Needanyhelp?",
}}
/
}
构建和运行这里就参照 Next.js 官方即可
package.json
"scripts": {
"dev-frontend":"pnpm i && next dev --turbopack",
"dev-agent-js":"cd agent-js && pnpm i && npx @langchain/langgraph-cli dev --host 0.0.0.0 --port 8123 --no-browser",
"dev-agent-py":"cd agent && poetry install && poetry run langgraph dev --host 0.0.0.0 --port 8123 --no-browser",
"dev":"pnpx concurrently \"pnpm dev-frontend\" \"pnpm dev-agent-js\" --names ui,agent --prefix-colors blue,green",
"build":"next build",
"start":"next start",
"lint":"next lint"
},
Server建议直接参考MCP Server Typescript SDK示例开发,官网文档的用法更新没那么及时,容易走弯路。
?mcp-server-supos是一个可用的 MCP Server,也发布了对应的npm 包。
?这里截取核心代码片段,想了解更多可点击查看源码和使用文档等。
核心代码提供tool-调用API查询信息实时订阅MQTT topic数据进行缓存,用于提供 tool 查询分析最新数据示例 server.resourceindex.ts
#!/usr/bin/env node
import{ McpServer }from"@modelcontextprotocol/sdk/server/mcp.js";
import{ StdioServerTransport }from"@modelcontextprotocol/sdk/server/stdio.js";
importfetchfrom"node-fetch";
import{ z }from"zod";
importfs, { readFileSync }from"fs";
import_from"lodash";
importmqttfrom"mqtt";
import{ pathToFileURL }from"url";
import{ createFilePath }from"./utils.js";
letSUPOS_API_URL =
process.env.SUPOS_API_URL;
letSUPOS_API_KEY =
process.env.SUPOS_API_KEY;
letSUPOS_MQTT_URL =
process.env.SUPOS_MQTT_URL;
if(!SUPOS_API_URL) {
console.error("SUPOS_API_URL environment variable is not set");
process.exit(1);
}
if(!SUPOS_API_KEY) {
console.error("SUPOS_API_KEY environment variable is not set");
process.exit(1);
}
constfilePath = createFilePath();
constfileUri = pathToFileURL(filePath).href;
asyncfunctiongetModelTopicDetail(topic: string):Promiseany{
consturl =`${SUPOS_API_URL}/open-api/supos/uns/model?topic=${encodeURIComponent(
topic
)}`;
constresponse =awaitfetch(url, {
headers: {
apiKey:`${SUPOS_API_KEY}`,
},
if(!response.ok) {
thrownewError(`SupOS API error:${response.statusText}`);
}
returnawaitresponse.json();
}
functiongetAllTopicRealtimeData(){
// 缓存实时数据,定时写入缓存文件
constcache =newMap();
lettimer: any =null;
constoptions = {
clean:true,
connectTimeout:4000,
clientId:"emqx_topic_all",
rejectUnauthorized:false,
reconnectPeriod:0,// 不进行重连
constconnectUrl = SUPOS_MQTT_URL;
if(!connectUrl) {
return;
}
constclient = mqtt.connect(connectUrl, options);
client.on("connect",function(){
client.subscribe("#",function(err){
// console.log("err", err);
client.on("message",function(topic, message){
cache.set(topic, message.toString());
client.on("error",function(error){
// console.log("error", error);
client.on("close",function(){
if(timer) {
clearInterval(timer);
}
// 每 5 秒批量写入一次
timer = setInterval(()={
constcacheJson =JSON.stringify(
Object.fromEntries(Array.from(cache)),
null,
2
// 将更新后的数据写入 JSON 文件
fs.writeFile(
filePath,
cacheJson,
{
encoding:"utf-8",
},
(error) = {
if(error) {
fs.writeFile(
filePath,
JSON.stringify({msg:"写入数据失败"},null,2),
{encoding:"utf-8"},
() = {}
}
}
},5000);
}
functioncreateMcpServer(){
constserver =newMcpServer(
{
name:"mcp-server-supos",
version:"0.0.1",
},
{
capabilities: {
tools: {},
},
}
// Static resource
server.resource("all-topic-realtime-data", fileUri,async(uri) = ({
contents: [
{
uri: uri.href,
text: readFileSync(filePath, {encoding:"utf-8"}),
},
],
}));
server.tool(
"get-model-topic-detail",
{topic: z.string() },
async(args: any) = {
constdetail =awaitgetModelTopicDetail(args.topic);
return{
content: [{type:"text",text:`${JSON.stringify(detail)}`}],
}
server.tool("get-all-topic-realtime-data", {},async() = {
return{
content: [
{
type:"text",
text: readFileSync(filePath, {encoding:"utf-8"}),
},
],
asyncfunctionrunServer(){
consttransport =newStdioServerTransport();
constserverConnect =awaitserver.connect(transport);
console.error("SupOS MCP Server running on stdio");
returnserverConnect;
}
runServer().catch((error) ={
console.error("Fatal error in main():", error);
process.exit(1);
}
asyncfunctionmain(){
try{
createMcpServer();
getAllTopicRealtimeData();
}catch(error) {
console.error("Error in main():", error);
process.exit(1);
}
}
main();
utils.ts
importfsfrom"fs";
importpathfrom"path";
exportfunctioncreateFilePath(
filedir: string =".cache",
filename: string ="all_topic_realdata.json"
){
// 获取项目根路径
constrootPath = process.cwd();
// 创建缓存目录
constfilePath = path.resolve(rootPath, filedir, filename);
constdirPath = path.dirname(filePath);
// 检查目录是否存在,如果不存在则创建
if(!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, {recursive:true
}
returnfilePath;
}
exportfunctionreadFileSync(filePath: string, options: any){
try{
returnfs.readFileSync(filePath, options);
}catch(err) {
return`读取文件时出错:${err}`;
}
}
如何使用「Client」:目前支持MCP协议的客户端已有很多,比如桌面端应用Claude for Desktop,或者IDE的一些插件等(VSCode的Cline插件),想了解已支持的客户端可访问Model Context Protocol Client
「Server」:除了官方例子Model Context Protocol Client外,已有很多网站整合了 MCP Servers,例如mcp.so,Glama等。
下面列举几个介绍下:
1. 配合本文 web 版 Client 使用(以todoist-mcp-server为例子)1)配置2)使用2. 配合 Claude 使用具体可参考:mcp-server-supos README.md,服务换成自己需要的即可
3. 使用VSCode的Cline插件由于使用npx找不到路径,这里以node执行本地文件为例
1)配置2)使用结语以上便是近期使用MCP的一点小经验~
整理完后看了下,如果只是单纯想集成些MCP Server,其实可以不用agent形式,直接使用copilotkit的标准模式,在本地服务调用langchainjs-mcp-adapters和 LLM 即可,例如:
import{
CopilotRuntime,
LangChainAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
}from'@copilotkit/runtime';
import{ ChatOpenAI }from"@langchain/openai";
import{ NextRequest }from'next/server';
// todo: 调用 @langchain/mcp-adapters 集成 MCP Server 获取 tools 给到大模型
...
const model =newChatOpenAI({model:"gpt-4o",apiKey: process.env.OPENAI_API_KEY });
constserviceAdapter =newLangChainAdapter({
chainFn:async({ messages, tools }) = {
returnmodel.bindTools(tools).stream(messages);
// or optionally enable strict mode
// return model.bindTools(tools, { strict: true }).stream(messages);
}
});
construntime =newCopilotRuntime();
exportconstPOST =async(req: NextRequest) = {
const{ handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint:'/api/copilotkit',
returnhandleRequest(req);
};
但这样可能少了些上下文状态等,具体可以下来都试试~
点击关注公众号,“技术干货” 及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线