LeonardoSya/weather-mcp-server
If you are the rightful owner of weather-mcp-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.
This document provides a comprehensive overview of building a Model Context Protocol (MCP) server using Node.js, specifically for a weather service.
How to build a MCP Server
使用Node.js编写一个MCP天气服务
准备工作
$ mkdir
$ npm init -y
$ npm install @modelcontextprotocol/sdk zod
$ npm install -D @types/node typescript
$ mkdir src
$ touch src/index.ts
/** package.json */
{
"name": "weather-mcp",
"version": "1.0.0",
"main": "index.js",
/** MCP SDK推荐使用ES模块 */
"type": "module",
"description": "",
/** 创建全局命令行工具,用户安装后可以直接运行weather命令启动这个MCP服务器,这是MCP服务器的标准分发模式*/
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
/** build脚本中,tsc用于编译ts代码,chmod 755设置执行权限,确保命令行工具可以运行 */
"build": "tsc && chmod 755 build/index.js"
},
/** 指定发布时只包含构建后的文件,可以减少包大小 */
"files": [
"build"
],
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.25.23"
},
"devDependencies": {
"@types/node": "^22.15.21",
"typescript": "^5.8.3"
}
}
/** tsconfig.json */
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
创建mcp server实例
/** src/index.ts */
// Create server instance
const server = new McpServer({
name: "weather",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
});
注册mcp server的工具
server.tool()
是mcp服务器向agent注册可用工具的方法。每次调用此方法,就相当于向agent宣告:“我这里有一个新的能力/工具,你可以在需要时调用它”
server.tool(
"工具名称", // 1. 工具标识符
"工具描述", // 2. 功能说明
参数模式对象, // 3. 参数契约
执行函数 // 4. 实际逻辑
)
通过第二个参数“工具描述”,agent就知道如何根据描述选择工具;通过第三个参数“参数模式对象”,agent就知道如何传递参数。因此agent和mcp server交互的思维示例如下:
server.tool代码如下(省略工具函数,可前往github查看):
/** src/index.ts */
// Register weather tools
/** agent询问工具“你能做什么?”,我的服务就会回应它拥有get-alerts和get-forecast这两个工具 */
server.tool(
"get-alerts",
"Get weather alerts for a state",
/** get-alerts需要一个state参数 */
/** 这个对象其实就是一份”参数说明书“(Schema),当服务器启动并与agent连接时,就会把这份说明书完整地发送给agent,agent就能知道参数名、类型和描述 */
{
state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
},
async ({ state }) => {
const stateCode = state.toUpperCase();
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
if (!alertsData) {
return {
content: [
{
type: "text",
text: "Failed to reterieve alerts data",
},
],
};
}
const features = alertsData.features || [];
if (features.length === 0) {
return {
content: [
{
type: "text",
text: `No active alerts for ${stateCode}`,
},
],
};
}
const formattedAlerts = features.map(formatAlert);
const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join(
"\n"
)}`;
return {
content: [
{
type: "text",
text: alertsText,
},
],
};
}
);
server.tool(
"get-forecast",
"Get weather forecast for a location",
/** get-forecast需要latitude和longitude参数 */
{
latitude: z.number().min(-90).max(90).describe("Latitude of a location"),
longitude: z
.number()
.min(-180)
.max(180)
.describe("Longitude of a location"),
},
async ({ latitude, longitude }) => {
// get grid point data
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(
4
)}, ${longitude.toFixed(4)}`;
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
if (!pointsData) {
return {
content: [
{
type: "text",
text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}`,
},
],
};
}
const forecastUrl = pointsData.properties?.forecast;
if (!forecastUrl) {
return {
content: [
{
type: "text",
text: "Failed to get forecast URL from grid point data",
},
],
};
}
// Get forecast data
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
if (!forecastData) {
return {
content: [
{
type: "text",
text: "Failed to retrieve forecast data",
},
],
};
}
const periods = forecastData.properties?.periods || [];
if (periods.length === 0) {
return {
content: [
{
type: "text",
text: "No forecast periods available",
},
],
};
}
// Format forecast periods
const formattedForecast = periods.map((period: ForecastPeriod) =>
[
`${period.name || "Unknown"}:`,
`Temperature: ${period.temperature || "Unknown"}°${
period.temperature || "F"
}`,
`Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
`${period.shortForecast || "No forecast available"}`,
"---",
].join("\n")
);
const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join(
"\n"
)}`;
return {
content: [
{
type: "text",
text: forecastText,
},
],
};
}
);
main fn
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
}
main().catch((err) => {
console.error("Fatal error in main():", err);
process.exit(1);
});
构建
$ npm run build
$ npm link --force
根据如下的package.json配置,npm build
会将src/index.ts编译成javascript,然后输出到build/index.js,然后赋予系统直接运行这个文件的权限(chmod 755)
/** package.json */
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
}
npm link
npm link
做两件事:在全局创建一个指向你本地项目的symbolic link(符号链接),然后将你项目的package.json中**bin
字段里指定的任何可执行文件**链接到系统的PATH
里
这样就能通过weather
命令直接启动这个node服务了
集成custom mcp server到cursor
在.cursor/mcp.json
中配置
{
"mcpServers": {
"weather": { // 这是mcp server的标识符
"command": "weather", // 这是要执行的命令(之前通过npm link创建的)
"args": [] // 命令行参数(目前为空,因为这个weather服务器不需要额外参数)
}
}
}
写完后重启cursor,发现mcp server能够被识别啦
新开一个chat view,发现cursor可以调用这个custom mcp了