一:什么是MCP Client和MCPserver

网上到处有解释,作者就不过多赘述了,简单来说可以通过MCP协议实现产品项目和大模型进行交互。

二:环境依赖

业务接口由springboot项目提供,后台管理系统使用vue3+vite,MCP项目使用的是node+typescript,所以所需的环境有:

后端:jdk1.8,mysql8.0,maven3.6,redis5.0

前端:node22.12.0,vue3。

三:项目说明

mall-backend:springboot项目

mall-mcp-client:mcp面向客户端

mall-mcp-server:mcp面向服务端

mall-web:后台管理前端代码

这个工作流程为:首先客户端(mall-web)通过接口,将提的问题传给mall-mcp-client,mall-mcp-client获取到数据之后,通过mcp sdk获取到mall-mcp-server提供的与之最符合的tools(tools实际上就是一个格式化后的接口数据),随后将获取到的tools提交给到大模型。支持mcp的大模型,会调用tools,等大模型调用好之后,mall-mcp-client将大模型的处理数据返回给到mall-web(前端)

温馨提示:使用MCP链接到数据库,只能用于查询和数据展示,最好不要对数据集进行写操作,因为大模型做写操作参数容易给错或者会抽风调用与之不相干的tools。

mall-mcp-client使用express实现https模式对外提供api

四:部分代码

前端项目mall-vite-web:vue3

下载依赖:

npm install

运行项目

npm run dev

const sendMessage = () => {
  if (message.value) {
    let user: any = JSON.parse(localStorage.getItem("user"));
    let token = "";
    if (user && user["token"]) {
      token = user.token;
    }
    sendLOading.value = true;
    const data = {
      role: "user",
      content: message.value,
      token,
    };

    messageList.value.push({
      role: "user",
      content: message.value,
    });

    message.value = "";

    nextTick(() => {
      scrollToBottom();
    });
    getAiquestion(data)
      .then((res: any) => {
        const messageData = {
          role: "assistant",
          content: res.data.message,
        };
        console.log("res", res);

        messageList.value.push(messageData);
        localStorage.setItem("messageList", JSON.stringify(messageList.value));
        nextTick(() => {
          scrollToBottom();
        });
      })
      .finally(() => {
        sendLOading.value = false;
      });
  }
};

mall-mcp-client项目

依赖下载

npm install

先打包再运行

打包

npm run build

运行

npm run dev

import { Client } from "@modelcontextprotocol/sdk/client";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import OpenAI from "openai";
import bodyParser from "body-parser";
const MODEL = "qwen-plus";
import express from "express";
const app = express();
app.use(express.json());
app.use(bodyParser.urlencoded({ extended: false }));

// 连接 mcp 服务器,这里使用node启动weather.js
// 请根据实际情况更改 weather.js 文件路径
const transport = new StdioClientTransport({
  command: "node",
  args: ["E:/template-mcp/mall-mcp-server/build/index.js"],
});
const mcpClient = new Client({
  name: "mcp-client",
  version: "1.0.0",
});
await mcpClient.connect(transport);
// 获取 mcp server tools 目前两个工具
const ret = await mcpClient.listTools();
const { tools } = ret;
console.log(
  "Connected to mcp server with tools:",
  tools.map(({ name }) => name)
);
console.log(`mcpTools: ${JSON.stringify(tools, null, 2)}\n`);
// 转换MCP tools到OpenAI格式
const convertToOpenAITools = (mcpTools: any) => {
  return mcpTools.map((tool: any) => ({
    type: "function",
    function: {
      name: tool.name,
      description: tool.description,
      parameters: tool.inputSchema,
    },
  }));
};
const openaiTools = convertToOpenAITools(tools);
console.log(`openaiTools: ${JSON.stringify(openaiTools, null, 2)}\n`);
// 初始化OpenAI
const openai = new OpenAI({
  apiKey: "sk-xxxxx",
  baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
});
// 调用OpenAI
async function callFunction(toolCall: any, token = "") {
  const args = JSON.parse(toolCall.function.arguments);
  const result = await mcpClient.callTool({
    name: toolCall.function.name,
    arguments: { ...args, token: token },
  });
  console.log(`工具返回结果: ${JSON.stringify(result, null, 2)}`);
  return result.content;
}
async function runConversation(query: string, token = "") {
  const messages: any = [{ role: "user", content: query }];
  // 初始调用,可能会触发工具调用
  const completion = await openai.chat.completions.create({
    model: "qwen-plus",
    messages,
    tools: openaiTools,
    tool_choice: "auto",
  });
  const responseMessage = completion.choices[0].message;
  // 第一次回复
  // 判断是否需要调用工具
  if (responseMessage.tool_calls) {
    // 将AI回复加入对话历史
    messages.push(responseMessage);
    // 处理每个工具调用
    for (const toolCall of responseMessage.tool_calls) {
      const result = await callFunction(toolCall, token);
      // 将工具调用结果加入对话历史
      messages.push({
        role: "tool",
        tool_call_id: toolCall.id,
        content: result,
      });
    }
    // 使用工具调用结果再次调用模型获取最终回复
    const secondCompletion = await openai.chat.completions.create({
      model: MODEL,
      messages,
    });
    const finalMessage = secondCompletion.choices[0].message;
    return finalMessage.content;
  } else {
    // 如果没有工具调用,直接返回AI的回复,同样的问题,模型可能会有不同的回答,但是不会再调用工具
    if (responseMessage.content) {
      return responseMessage.content;
    } else {
      return "没有内容回复";
    }
  }
}
app.post("/questioning-ssm", (req, res: any) => {
  console.log("请求数据", req.body);
  let content = req.body.content;
  let token = req.body.token;
  // 运行对话流程
  runConversation(content, token)
    .then((result) => {
      res.setHeader("Content-Type", "application/json; charset=utf-8");
      // 返回 JSON 数据
      res.send(JSON.stringify({ message: result }));
      // res.send(result);
    })
    .catch((error) => {
      console.error("发生错误:", error);
      process.exit(1);
    });
});

app.listen(3043, () => {
  console.log("示例应用正在监听 3043 端口!");
});

其中

  args: ["E:/template-mcp/mall-mcp-server/build/index.js"],

表示mall-mcp-server打包后的主入口文件,apiKey我用的是通义千问的,这个可以到通义千问官网自行申请获取。

mall-mcp-server项目

由于该项目是通过对外提供入口文件的方式,只需要打包项目即可,无需运行

下载依赖

npm install

打包项目

npm run build

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  getGoodListTool,
  geRandListTool,
  getShangChengTool,
  getModelInfoTool,
  getContactTool,
  getCreateProductTool,
  delProductTool,
  getCategoryTool,
  getSaleDateToole,
} from "./tools/index";
import { z } from "zod";
// 创建 server instance
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
  capabilities: {
    resources: {},
    tools: {},
  },
});
// Helper function 用于发送 NWS API 请求
server.tool("get-good-list", "获取到产品列表", {}, async () => {
  let infoList: any = await getGoodListTool();
  return infoList;
});
// Helper function 用于发送 NWS API 请求
server.tool(
  "get-rand-list",
  "获取销售列表",
  {
    token: z.string().describe("用户token"),
    num: z.string().describe("前多少名销售排行"),
  },
  async ({ num, token }) => {
    let data = {
      num,
      token,
    };
    let infoList: any = await geRandListTool(data);
    return infoList;
  }
);
// Helper function 用于发送 NWS API 请求
server.tool("get-kefu-info", "获取模型信息", {}, async () => {
  let infoList: any = await getModelInfoTool();
  return infoList;
});
server.tool("get-shangcheng-info", "获取主营信息", {}, async () => {
  let infoList: any = await getShangChengTool();
  return infoList;
});
server.tool("get-contact-info", "获取联系方式", {}, async () => {
  let infoList: any = await getContactTool();
  return infoList;
});
server.tool(
  "create-product-tool",
  "创建商品",
  {
    discount: z
      .string()
      .describe("折扣:无折扣为1,9折为0.9,8折为0.8,依次类推"),
    name: z.string().describe("商品名称"),
    description: z.string().describe("商品描述"),
    categoryId: z.string().describe("商品分类id"),
    imgs: z.string().describe("商品封面"),
    categoryList: z.array(
      z
        .object({
          value: z.string().describe("规格名称"),
          price: z.string().describe("该商品规格对应的价格"),
          store: z.string().describe("商品规格对应的库存"),
        })
        .describe("产品规格对应的信息数组")
    ),
  },
  async ({ discount, name, description, categoryId, imgs, categoryList }) => {
    let categoryArr: any = await getCategoryTool();
    let _categoryId = "";
    if (isNaN(Number(categoryId))) {
      let _data = categoryArr.filter((item: any) => {
        return item.name.includes(categoryId);
      });
      if (_data.length > 0) {
        _categoryId = _data[0].id;
      }
    } else {
      _categoryId = categoryId;
    }

    let data = {
      discount,
      name,
      description,
      categoryId: _categoryId,
      imgs,
      categoryList,
    };
    let infoList: any = await getCreateProductTool(data);
    return infoList;
  }
);
server.tool(
  "del-product-tool",
  "通过is删除商品",
  {
    id: z.string().describe("需要删除的产品id"),
  },
  async ({ id }) => {
    let infoList: any = await delProductTool(id);
    return infoList;
  }
);
// getSaleDateToole
server.tool(
  "get-sale-date-tool",
  "获取到销售情况",
  {
    token: z.string().describe("权限token"),
  },
  async ({ token }) => {
    let infoList: any = await getSaleDateToole(token);
    return infoList;
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

其中引入的例如getGoodListTool为查询产品列表后返回的数据,该返回格式为:

{
   content: [
        {
          type: "text",
          text: "未能检索警报数据",
        },
      ]
}

具体详情可下载项目代码自行观看。

mall-backend

这个不说了 ,就是springboot项目,用来为mcp提供数据库数据。怎么运行springboot项目网上搜就行了

五:测试情况

六:开源项目下载地址

以下为本例子的gitee下载地址,可自行下载,如果觉得这个开源小项目对你学习mcp有帮助,可以给个star!!!在此感谢!

https://gitee.com/waywordcode/template-mcp-mall

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐