使用NodeJS实现自定义MCP服务,并发布到NPM以及MCP客户端(Cherry Studio)如何连接使用教程

1.创建使用npm init -y创建一个空白的项目

1.1新建一个空白的文件夹,打开终端,直接执行npm init -y 执行成功之后效果如下,会生产一个package.json文件

在这里插入图片描述
用idea或其他开发工具打开项目,将package.json的内容替换掉

{
  "name": "mcptest-lmk",
  "version": "1.0.0",
  "description": "测试mcp服务发布",
  "type": "module",
  "bin": {
    "mcptest-lmk": "build/weather-server.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
  },
  "keywords": [
    "mcp",
    "weather",
    "ai",
    "claude",
    "cline",
    "meizu"
  ],
  "author": "yingzi",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "node-fetch": "^3.3.2",
    "string-similarity": "^4.0.4",
    "zod": "^3.22.4",
    "dotenv": "^16.4.7",
    "mysql2": "^3.11.5"
  },
  "devDependencies": {
    "nodemon": "^3.0.3"
  }
}

运行npm install安装依赖
在这里插入图片描述

2.MCP服务器代码编写

MCP服务器开发官网文档
官网上有详细的server代码教程

2.1我们这里直接在build文件夹下新建一个weather-server.js文件,因为等会打包上传到npm的时候只有build下的文件才会上传,为了简单就直接这样操作了,正常的node开发应该是要使用打包指令打包项目的。

在这里插入图片描述

2.2 具体实现代码如下,直接复制即可用

#!/usr/bin/env node
console.log("Hello World!");

import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";

import fetch from "node-fetch";
import {z} from "zod";

// 创建MCP服务器
const server = new McpServer({
    name: "weather-mcp",
    version: "1.0.0"
});

// 城市ID映射表(部分常用城市)
const CITY_MAP = {
    "北京": "101010100",
    "上海": "101020100",
    "武汉": "101200101",
    // 可以按需添加更多城市
};

// 注册城市天气查询工具
server.tool(
    "query_weather",
    {
        city: z.string().describe("要查询天气的城市名称,如北京、上海、广州等")
    },
    async ({city}) => {
        try {
            let cityId = CITY_MAP[city];

            // 如果城市不在映射中,返回提示信息
            if (!cityId) {
                return {
                    content: [{
                        type: "text",
                        text: `暂不支持查询${city}的天气信息。目前支持的城市有: ${Object.keys(CITY_MAP).join("、")}`
                    }]
                };
            }

            // 构建魅族天气API URL
            const url = `http://aider.meizu.com/app/weather/listWeather?cityIds=${cityId}`;

            // 发送请求获取天气数据
            const response = await fetch(url);

            if (!response.ok) {
                throw new Error(`天气API返回错误: ${response.status} ${response.statusText}`);
            }

            const data = await response.json();

            // 检查API返回状态
            if (data.code !== "200" || !data.value || !data.value[0]) {
                throw new Error(`获取天气数据失败: ${data.message || "未知错误"}`);
            }

            const weatherData = data.value[0];

            // 获取实时天气
            const realtime = weatherData.realtime;
            // 获取今日天气
            const today = weatherData.weathers.find(w => {
                const today = new Date();
                const dateStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;
                return w.date === dateStr;
            }) || weatherData.weathers[0];

            // 获取生活指数
            const indexes = weatherData.indexes || [];

            // 格式化天气信息
            let result = `🌍 ${weatherData.city} ${weatherData.provinceName} 实时天气\n`;
            result += `${realtime.time}\n\n`;
            result += `🌡 气温温度: ${realtime.temp}℃ (体感温度: ${realtime.sendibleTemp}℃)\n`;
            result += `🌤 天气状况: ${realtime.weather}\n`;
            result += `💨 湿度: ${realtime.sd}%\n`;
            result += `🌬 风向风力: ${realtime.wd} ${realtime.ws}\n\n`;

            result += `🌡 今日温度: ${today.temp_day_c}℃ / ${today.temp_night_c}℃\n`;
            result += `☀️ 日出/日落: ${today.sun_rise_time} / ${today.sun_down_time}\n\n`;

            // 添加空气质量信息(如果有)
            if (weatherData.pm25) {
                result += `😷 空气质量: ${weatherData.pm25.quality} (AQI: ${weatherData.pm25.aqi})\n`;
                result += `💭 PM2.5: ${weatherData.pm25.pm25}, PM10: ${weatherData.pm25.pm10}\n\n`;
            }

            // 添加生活指数
            if (indexes.length > 0) {
                result += `🔖 生活指数参考\n`;
                indexes.forEach(index => {
                    // 添加对应的emoji
                    let emoji = "📌";
                    switch (index.abbreviation) {
                        case "ct":
                            emoji = "🌡";
                            break; // 穿衣
                        case "pp":
                            emoji = "☂";
                            break; // 化妆
                        case "mm":
                            emoji = "🎭";
                            break; // 感冒
                        case "xc":
                            emoji = "🧺";
                            break; // 洗车
                        case "yd":
                            emoji = "🏃";
                            break; // 运动
                        case "uv":
                            emoji = "☀";
                            break; // 紫外线
                    }
                    result += `${emoji} ${index.name}(${index.level}): ${index.content}\n`;
                });
            }

            return {
                content: [{type: "text", text: result.trim()}]
            };
        } catch (error) {
            console.error("获取天气时出错了:", error);
            return {
                content: [{type: "text", text: `查询天气时出错: ${error.message}`}],
                isError: true
            };
        }
    }
);

// 注册天气预报工具
server.tool(
    "query_forecast",
    {
        city: z.string().describe("要查询天气预报的城市名称")
    },
    async ({city}) => {
        try {
            let cityId = CITY_MAP[city];

            // 如果城市不在映射表中,返回提示信息
            if (!cityId) {
                return {
                    content: [{
                        type: "text",
                        text: `暂不支持查询${city}的天气预报。目前支持的城市有: ${Object.keys(CITY_MAP).join("、")}`
                    }]
                };
            }

            // 构建魅族天气API URL
            const url = `http://aider.meizu.com/app/weather/listweather?cityIds=${cityId}`;

            // 发送请求获取天气数据
            const response = await fetch(url);

            if (!response.ok) {
                throw new Error(`天气API返回错误: ${response.status} ${response.statusText}`);
            }

            const data = await response.json();

            // 检查API返回状态
            if (data.code !== "200" || !data.value || !data.value[0]) {
                throw new Error(`获取天气数据失败: ${data.message || "未知错误"}`);
            }

            const weatherData = data.value[0];

            // 获取天气预报
            const forecasts = weatherData.weathers || [];

            // 按日期排序(确保顺序正确)
            forecasts.sort((a, b) => {
                const dateA = new Date(a.date);
                const dateB = new Date(b.date);
                return dateA - dateB;
            });

            // 排除过去的日期
            const today = new Date();
            today.setHours(0, 0, 0, 0);

            // 过滤出未来天气预报
            const futureForecasts = forecasts.filter(forecast => {
                const forecastDate = new Date(forecast.date);
                return forecastDate >= today;
            });

            // 格式化天气预报
            let result = `🌍 ${weatherData.city}未来天气预报:\n\n`;

            futureForecasts.forEach((forecast, index) => {
                const date = new Date(forecast.date);
                const monthDay = `${date.getMonth() + 1}${date.getDate()}`;

                // 添加天气图标
                let weatherEmoji = "🌤";
                if (forecast.weather.includes("晴")) weatherEmoji = "☀";
                else if (forecast.weather.includes("雨")) weatherEmoji = "🌧";
                else if (forecast.weather.includes("云")) weatherEmoji = "☁";
                else if (forecast.weather.includes("阴")) weatherEmoji = "⛅";
                else if (forecast.weather.includes("雪")) weatherEmoji = "❄";

                result += `${index === 0 ? "📅 今天" : "📅 " + monthDay} ${forecast.week}:\n`;
                result += `${weatherEmoji} ${forecast.weather}\n`;
                result += `🌡 ${forecast.temp_day_c}℃ / ${forecast.temp_night_c}℃\n`;
                result += `${forecast.sun_rise_time} - ${forecast.sun_down_time}\n\n`;
            });

            // 添加生活小贴士
            const indexes = weatherData.indexes || [];
            if (indexes.length > 0) {
                const randomIndex = Math.floor(Math.random() * indexes.length);
                result += `💡 今日小贴士: ${indexes[randomIndex].content}\n`;
            }

            return {
                content: [{type: "text", text: result.trim()}]
            };
        } catch (error) {
            console.error("获取天气预报时出错:", error);
            return {
                content: [{type: "text", text: `查询天气预报时出错: ${error.message}`}],
                isError: true
            };
        }
    }
);

// 注册每小时天气预报工具
server.tool(
    "query_hourly_forecast",
    {
        city: z.string().describe("要查询每小时天气预报的城市名称")
    },
    async ({city}) => {
        try {
            let cityId = CITY_MAP[city];
            console.log('1111111111111111',city)
            debugger
            // 如果城市不在映射表中,返回提示信息
            if (!cityId) {
                return {
                    content: [{
                        type: "text",
                        text: `暂不支持查询${city}的精细天气预报。目前支持的城市有: ${Object.keys(CITY_MAP).join("、")}`
                    }]
                };
            }

            // 构建魅族天气API URL
            const url = `http://aider.meizu.com/app/weather/listweather?cityIds=${cityId}`;

            // 发送请求获取天气数据
            const response = await fetch(url);

            if (!response.ok) {
                throw new Error(`天气API返回错误: ${response.status} ${response.statusText}`);
            }

            const data = await response.json();

            // 检查API返回状态
            if (data.code !== "200" || !data.value || !data.value[0]) {
                throw new Error(`获取天气数据失败: ${data.message || "未知错误"}`);
            }

            const weatherData = data.value[0];

            // 获取精细天气预报
            const hourlyForecasts = weatherData.weatherDetailsInfo?.weather3HoursDetailsInfos || [];

            if (hourlyForecasts.length === 0) {
                return {
                    content: [{type: "text", text: `暂无${city}未来几小时的精细天气预报数据`}]
                };
            }

            // 格式化精细天气预报
            let result = `🌍 ${weatherData.city}未来3小时天气预报:\n\n`;

            hourlyForecasts.forEach(forecast => {
                const startTime = new Date(forecast.startTime);
                const endTime = new Date(forecast.endTime);

                const startHour = startTime.getHours();
                const endHour = endTime.getHours();

                // 添加天气图标
                let weatherEmoji = "🌤";
                if (forecast.weather.includes("晴")) weatherEmoji = "☀";
                else if (forecast.weather.includes("雨")) weatherEmoji = "🌧";
                else if (forecast.weather.includes("云")) weatherEmoji = "☁";
                else if (forecast.weather.includes("阴")) weatherEmoji = "⛅";
                else if (forecast.weather.includes("雪")) weatherEmoji = "❄";

                result += `${startHour}:00-${endHour}:00\n`;
                result += `${weatherEmoji} ${forecast.weather}\n`;
                result += `🌡 ${forecast.lowestTemperature}℃ - ${forecast.highestTemperature}℃\n`;

                // 添加降水信息(如果有)
                if (forecast.precipitation && forecast.precipitation !== "0") {
                    result += `💧 降水量: ${forecast.precipitation}mm\n`;
                }

                result += `\n`;
            });

            return {
                content: [{type: "text", text: result.trim()}]
            };
        } catch (error) {
            console.error("获取精细天气预报时出错:", error);
            return {
                content: [{type: "text", text: `获取精细天气预报时出错: ${error.message}`}],
                isError: true
            };
        }
    }
);

server.tool(
    "getEmployeeInfo",
    {
        emName: z.string().describe("要查询的员工的姓名,(例如张三,李四)")
    },
    async ({emName}) => {
        try {
            return {
                content: [{type: "text", text: "成功进入工具,AI识别到的参数列表," + emName.toString()}]
            };
        } catch (error) {
            console.error("要查询的员工的姓名出错:", error);
            return {
                content: [{type: "text", text: `要查询的员工的姓名出错: ${error.message}`}],
                isError: true
            };
        }
    }
);




// 启动服务器
async function main() {
    try {
        // 打印服务器启动信息
        console.log("启动天气查询的MCP服务器...");

        // 使用标准输入/输出作为传输方式
        const transport = new StdioServerTransport();
        await server.connect(transport);

        console.log("MCP服务器已经开始等待连接");
    } catch (error) {
        console.error("启动服务器时出错:", error);
        process.exit(1);
    }
}

main();

主要注册了四个工具方法,具体的逻辑代码不重要,可以随意编写,本文重点讲关于发布到npm以及使用mcp客户端连接的问题
在这里插入图片描述

2.3 使用Cherry Studio本地测试

服务代码写完之后就可以使用客户端先本地连接测试了
CherryStudio如何开启MCP服务这里也不多说了,直接上CherryStudio查看,很简单
打开MCP服务加入配置

    "mcp本地测试": {
      "name": "mcp本地测试",
      "type": "stdio",
      "isActive": true,
      "command": "node",
      "args": [
        "D:\\Zszj2025D\\MCPServerTest\\build\\weather-server.js"
      ]
    }

args中的路径参数一定要指向我们编写的那个js文件,使用绝对路径

参数配置号之后点保存,亮绿灯了,并且下面出现可用工具列表就说明连接成功了,
在这里插入图片描述

2.4本地连接效果测试

点击上方的模型可以选择模型提问,右侧带一个小扳手的说明模型支持函数调用功能,只有支持函数调用功能的才可以使用MCP服务,
在这里插入图片描述

点击菜单栏的mcp服务器按钮,选择我们刚刚添加的服务,就可以开始提问了
在这里插入图片描述

可以看到调用了我们代码李德query_forecast方法,由于我们代码中不包含杭州这个城市,所有不支持查询,我们再问一次

在这里插入图片描述

在这里插入图片描述

3测试了工具没问题就可以开始发布了

3.1 发布到npm官网

没有npm账号的要先去注册一个,注册成功之后在终端使用npm login登录一下就行
我们一般使用的都是淘宝镜像,现在要换成npm的官网镜像

npm config set registry https://registry.npmjs.org/

在这里插入图片描述

切换之后再使用npm whoami查看登录状态,出现了我们注册的用户名说明没问题,可以开始发布了
在这里插入图片描述
发布之前我们要对package.json进行一些编辑,package.json中name是我们发布之后的报名,在npm官网中是唯一的,先用先得,bin和file是mcp的重要配置,files是我们要上传的文件,bin是代表npm的一个指令,如果在终端执行这个指令,实际上运行的是build/weather-server.js文件

   "name": "mcptest-lmk",
  "version": "1.0.0",
  "description": "测试mcp服务发布",
  "type": "module",
  "bin": {
    "mcptest-lmk": "build/weather-server.js"
  },
  "files": [
    "build"
  ],

一切准备就绪,使用npm publish发布,发布成功,我的包名是mcptest-lmk,可以直接用这个包名去npm官网搜索了,也可以查看自己的个人空间
在这里插入图片描述

在个人空间可以看到我们刚刚发布的包
在个人空间可以看到我们刚刚发布的包

3.2 使用CherryStudio连接我们发布的包

使用之前要先npm i mcptest-lmk -g ,把包下载到本地,实际上也是一种本地连接,只是包上传到了npm,让其他人方便使用
执行npm i mcptest-lmk -g ,全局安装这个包

在这里插入图片描述

在CherryStudio中添加配置,与上面的本地配置不一样的是command指令改成npx,args中的参数改成我们package.json文件中的bin配置的指令名,我这里配置的是mcptest-lmk,与包名一致

"mcp发包测试": {
      "name": "mcp发包测试",
      "type": "stdio",
      "isActive": true,
      "command": "npx",
      "args": [
        "mcptest-lmk"
      ]
    }

配置号之后点保存,也是直接亮绿灯了,可以使用了
在这里插入图片描述

可以看到工具方法是可以正常调用的,只是方法内部出现了问题,这里的问题是
我们方法里面访问的一个天气api崩了, http://aider.meizu.com/app/weather/listweather?cityIds,并不是连接上的问题,
在这里插入图片描述

结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值