一、项目初始化
创建项目
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
TypeScript 配置
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
二、基础 Server
创建 Server
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// 列出工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'hello',
description: '打招呼',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '名字',
},
},
required: ['name'],
},
},
],
}));
// 执行工具
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'hello') {
const { name } = request.params.arguments as { name: string };
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// 启动 Server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Server running on stdio');
}
main().catch(console.error);
三、实战:天气查询 Server
安装依赖
npm install axios
实现天气查询
import axios from 'axios';
interface WeatherTool {
name: 'get_weather';
arguments: {
city: string;
};
}
// 注册工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_weather',
description: '获取城市天气信息',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,如:北京、上海',
},
},
required: ['city'],
},
},
],
}));
// 执行工具
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'get_weather') {
const { city } = request.params.arguments as WeatherTool['arguments'];
try {
// 调用天气 API
const response = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: process.env.WEATHER_API_KEY,
q: city,
lang: 'zh',
},
}
);
const { current, location } = response.data;
return {
content: [
{
type: 'text',
text: `${location.name} 当前天气:
温度:${current.temp_c}°C
体感温度:${current.feelslike_c}°C
天气:${current.condition.text}
湿度:${current.humidity}%
风速:${current.wind_kph} km/h`,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `获取天气失败:${error.message}`,
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
四、添加 Resources
Resources 让 AI 能读取数据。
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
// 列出资源
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const files = await fs.readdir('./data');
return {
resources: files.map(file => ({
uri: `file:///${path.join(process.cwd(), 'data', file)}`,
name: file,
mimeType: 'text/plain',
description: `数据文件:${file}`,
})),
};
});
// 读取资源
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri);
const filePath = url.pathname;
try {
const content = await fs.readFile(filePath, 'utf-8');
return {
contents: [
{
uri: request.params.uri,
mimeType: 'text/plain',
text: content,
},
],
};
} catch (error) {
throw new Error(`Failed to read file: ${error.message}`);
}
});
五、配置 Claude Desktop
编译项目
npx tsc
配置文件
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"],
"env": {
"WEATHER_API_KEY": "your-api-key"
}
}
}
}
重启 Claude Desktop
配置生效后,在 Claude 中测试:
请查询北京的天气
六、进阶:数据库查询 Server
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'query_database',
description: '执行 SQL 查询',
inputSchema: {
type: 'object',
properties: {
sql: {
type: 'string',
description: 'SQL 查询语句',
},
},
required: ['sql'],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'query_database') {
const { sql } = request.params.arguments as { sql: string };
// 安全检查:只允许 SELECT
if (!sql.trim().toLowerCase().startsWith('select')) {
return {
content: [
{
type: 'text',
text: '只允许执行 SELECT 查询',
},
],
isError: true,
};
}
try {
const result = await pool.query(sql);
return {
content: [
{
type: 'text',
text: JSON.stringify(result.rows, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `查询失败:${error.message}`,
},
],
isError: true,
};
}
}
});
七、错误处理
统一错误处理
class MCPError extends Error {
constructor(
message: string,
public code: string,
public details?: any
) {
super(message);
this.name = 'MCPError';
}
}
// 包装工具执行
async function executeTool(name: string, args: any) {
try {
// 执行工具逻辑
return await toolHandlers[name](args);
} catch (error) {
if (error instanceof MCPError) {
return {
content: [
{
type: 'text',
text: `错误 [${error.code}]: ${error.message}`,
},
],
isError: true,
};
}
throw error;
}
}
八、测试
单元测试
import { describe, it, expect } from 'vitest';
describe('Weather Tool', () => {
it('should return weather data', async () => {
const result = await executeTool('get_weather', { city: '北京' });
expect(result.content[0].text).toContain('温度');
expect(result.content[0].text).toContain('°C');
});
it('should handle invalid city', async () => {
const result = await executeTool('get_weather', { city: 'InvalidCity123' });
expect(result.isError).toBe(true);
});
});
九、发布
发布到 npm
// package.json
{
"name": "@yourname/mcp-weather",
"version": "1.0.0",
"bin": {
"mcp-weather": "./dist/index.js"
},
"files": ["dist"]
}
npm publish
使用
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "@yourname/mcp-weather"]
}
}
}
十、最佳实践
1. 安全性
- 验证输入参数
- 限制权限范围
- 使用环境变量存储密钥
2. 性能
- 缓存频繁请求
- 设置超时
- 限流
3. 可维护性
- 清晰的工具描述
- 完善的错误信息
- 详细的日志
4. 用户体验
- 友好的错误提示
- 合理的默认值
- 丰富的示例