🚀打造你的第一个 MCP 服务器:完整指南
Source: Dev.to
介绍
模型上下文协议(MCP)正在革新 AI 应用与外部工具和数据源的交互方式。无论你是在构建 Claude 集成、VSCode 扩展,还是自定义 AI 工作流,了解 MCP 服务器都是现代 AI 开发的必备技能。
在本指南中,我们将从零开始构建一个功能完整的 MCP 服务器,将其部署到云端,并连接到流行的 MCP 客户端。完成后,你将拥有一个可供任何 MCP 客户端使用的天气信息服务器。
MCP(Model Context Protocol)是一种开放协议,标准化了 AI 应用如何连接外部数据源和工具。可以把它看作是一个通用适配器,使得 Claude 等 AI 模型能够安全地访问你的数据库、API、文件系统和业务工具。
- 标准化集成: 一个协议即可兼容所有 MCP 客户端
- 安全优先: 内置身份验证和权限控制
- 灵活架构: 支持本地和远程服务器
- 工具发现: 客户端可自动发现可用功能
一个 MCP 服务器由三个主要组件组成:
- 资源 – 服务器可以提供的数据源(文件、数据库记录、API 数据)
- 工具 – 服务器可以执行的操作(创建、更新、删除等)
- 提示 – 常见任务的预定义模板
下面我们将构建一个实用的 MCP 服务器,使用公共 API 提供天气信息。
项目设置
mkdir weather-mcp-server
cd weather-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk axios dotenv
npm install -D @types/node typescript
项目结构
weather-mcp-server/
├── src/
│ ├── index.ts
│ ├── server.ts
│ └── weatherService.ts
├── package.json
├── tsconfig.json
└── .env
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
package.json(脚本)
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js"
},
"type": "module"
}
天气服务 (src/weatherService.ts)
import axios from 'axios';
export interface WeatherData {
location: string;
temperature: number;
condition: string;
humidity: number;
windSpeed: number;
description: string;
}
export class WeatherService {
private apiKey: string;
private baseUrl = 'https://api.openweathermap.org/data/2.5';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async getCurrentWeather(city: string): Promise {
try {
const response = await axios.get(`${this.baseUrl}/weather`, {
params: {
q: city,
appid: this.apiKey,
units: 'metric'
}
});
const data = response.data;
return {
location: `${data.name}, ${data.sys.country}`,
temperature: Math.round(data.main.temp),
condition: data.weather[0].main,
humidity: data.main.humidity,
windSpeed: data.wind.speed,
description: data.weather[0].description
};
} catch (error) {
throw new Error(`Failed to fetch weather data: ${error}`);
}
}
async getForecast(city: string, days: number = 5): Promise {
try {
const response = await axios.get(`${this.baseUrl}/forecast`, {
params: {
q: city,
appid: this.apiKey,
units: 'metric',
cnt: days * 8 // API returns 3‑hour intervals
}
});
return response.data;
} catch (error) {
throw new Error(`Failed to fetch forecast data: ${error}`);
}
}
}
MCP 服务器 (src/server.ts)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import { WeatherService } from './weatherService.js';
export class WeatherMCPServer {
private server: Server;
private weatherService: WeatherService;
constructor(apiKey: string) {
this.weatherService = new WeatherService(apiKey);
this.server = new Server(
{
name: 'weather-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {}
},
}
);
this.setupHandlers();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_current_weather',
description: 'Get current weather information for a city',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name (e.g., "London", "New York")'
}
},
required: ['city']
}
},
{
name: 'get_forecast',
description: 'Get weather forecast for upcoming days',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name'
},
days: {
type: 'number',
description: 'Number of days (1-5)',
default: 5
}
},
required: ['city']
}
}
]
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'get_current_weather') {
const weather = await this.weatherService.getCurrentWeather(
args.city as string
);
return {
content: [
{
type: 'text',
text: JSON.stringify(weather, null, 2)
}
]
};
}
if (name === 'get_forecast') {
const forecast = await this.weatherService.getForecast(
args.city as string,
(args.days as number) || 5
);
return {
content: [
{
type: 'text',
text: JSON.stringify(forecast, null, 2)
}
]
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error}`
}
],
isError: true
};
}
});
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'weather://cities',
name: 'Popular Cities',
description: 'List of popular cities for weather queries',
mimeType: 'application/json'
}
]
}));
// Read resources
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'weather://cities') {
const cities = [
'London', 'New York', 'Tokyo', 'Paris', 'Sydney',
'Berlin', 'Toronto', 'Singapore', 'Dubai', 'Mumbai'
];
return {
contents: [
{
type: 'application/json',
data: JSON.stringify(cities, null, 2)
}
]
};
}
// Fallback for unknown resources
return {
contents: [],
isError: true,
errorMessage: `Resource not found: ${uri}`
};
});
}
// Start the server (e.g., using stdio transport)
public start() {
const transport = new StdioServerTransport(this.server);
transport.start();
}
}
运行服务器
创建一个 .env 文件并填入你的 OpenWeatherMap API 密钥:
OPENWEATHER_API_KEY=your_api_key_here
然后启动服务器:
npm run build
npm start
服务器将通过标准输入/输出监听,随时准备让任何兼容 MCP 的客户端(例如 Claude)发现 get_current_weather 与 get_forecast 工具,以及 weather://cities 资源。