HelloAI
L4 第 11 篇 🐣 难度 🕒 12 分钟

MCP 工具开发:手把手做一个自己的 MCP server

L4-07 讲了 MCP 是什么。这一篇手把手教你写一个能让 Claude / Cursor 直接用的 MCP server。

阿莱
2026/8/23

L4-07 讲过 MCP 是 “AI 工具的 USB”—— 这一篇我们实际做一个

读完你能:

  • 写一个能让 Claude Desktop / Cursor 等使用的 MCP server
  • 暴露任何你需要的工具能力
  • 加入 MCP 生态

选择语言

MCP 支持多种语言:

  • TypeScript / Node(最成熟)
  • Python(也很好)
  • Go / Rust(小众但能用)

本文用 Python—— 最适合 AI / 数据场景

安装

pip install mcp

最简 MCP Server

让我们做一个简单的”天气查询”工具:

# server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# 创建 server
server = Server("weather-tool")

# 声明工具
@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="get_weather",
            description="Get current weather for a city",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name (e.g., 'Beijing', 'San Francisco')"
                    }
                },
                "required": ["city"]
            }
        )
    ]

# 工具调用处理
@server.call_tool()
async def call_tool(name, arguments):
    if name == "get_weather":
        city = arguments["city"]
        # 这里就是你的真实业务逻辑
        weather = await fetch_weather_api(city)
        return [
            TextContent(
                type="text",
                text=f"Current weather in {city}: {weather}"
            )
        ]
    raise ValueError(f"Unknown tool: {name}")

# 实际查天气的函数
async def fetch_weather_api(city):
    # 调真实 weather API,比如 OpenWeatherMap
    # 这里简化
    return "Sunny, 22°C"

# 启动 server
if __name__ == "__main__":
    import asyncio
    async def main():
        async with stdio_server() as (read_stream, write_stream):
            await server.run(read_stream, write_stream, server.create_initialization_options())
    asyncio.run(main())

就这样——一个完整的 MCP server。

让 Claude Desktop 用它

1. 找到 Claude Desktop 配置文件

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

2. 添加 server 配置

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}

3. 重启 Claude Desktop

4. 用!

在 Claude Desktop 里: “What’s the weather in Beijing?”

Claude 自动调用你的 weather server—— 返回真实天气。

这就是 MCP 的魔力—— 工具 + LLM = 任意组合。

添加更复杂的工具

让我们加更多工具:

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="get_weather",
            description="Get current weather for a city",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string"}
                },
                "required": ["city"]
            }
        ),
        Tool(
            name="get_forecast",
            description="Get 5-day forecast",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "days": {"type": "integer", "default": 5, "maximum": 10}
                },
                "required": ["city"]
            }
        ),
        Tool(
            name="compare_cities",
            description="Compare weather between multiple cities",
            inputSchema={
                "type": "object",
                "properties": {
                    "cities": {
                        "type": "array",
                        "items": {"type": "string"}
                    }
                },
                "required": ["cities"]
            }
        ),
    ]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "get_weather":
        return await get_weather_handler(arguments)
    elif name == "get_forecast":
        return await get_forecast_handler(arguments)
    elif name == "compare_cities":
        return await compare_cities_handler(arguments)
    raise ValueError(f"Unknown tool: {name}")

每个工具一个 handler——清晰分工。

添加 Resources

不只是工具——也暴露可读资源

from mcp.types import Resource

@server.list_resources()
async def list_resources():
    return [
        Resource(
            uri="weather://config",
            name="Weather Service Config",
            mimeType="application/json"
        ),
        Resource(
            uri="weather://history/beijing",
            name="Beijing Weather History",
            mimeType="text/csv"
        ),
    ]

@server.read_resource()
async def read_resource(uri):
    if uri == "weather://config":
        return json.dumps({"api_key": "...", "units": "celsius"})
    elif uri.startswith("weather://history/"):
        city = uri.split("/")[-1]
        return load_weather_history(city)
    raise ValueError(f"Unknown resource: {uri}")

LLM 能 “读” 这些 resource—— 让它了解上下文。

添加 Prompts

预定义的工作流:

from mcp.types import Prompt

@server.list_prompts()
async def list_prompts():
    return [
        Prompt(
            name="weather_report",
            description="Generate a daily weather report",
            arguments=[
                {"name": "city", "description": "City name", "required": True}
            ]
        )
    ]

@server.get_prompt()
async def get_prompt(name, arguments):
    if name == "weather_report":
        city = arguments["city"]
        return {
            "description": f"Weather report for {city}",
            "messages": [
                {
                    "role": "user",
                    "content": {
                        "type": "text",
                        "text": f"""Please generate a daily weather report for {city}:
1. Use the get_weather tool to fetch current conditions
2. Use the get_forecast tool for the next 5 days
3. Format as: Current → Trend → Recommendations
"""
                    }
                }
            ]
        }

用户点 “weather_report” 预设 → Claude 自动按流程做。

生产级实现要点

1. 错误处理

@server.call_tool()
async def call_tool(name, arguments):
    try:
        if name == "get_weather":
            city = arguments.get("city")
            if not city:
                return [TextContent(type="text", text="Error: city is required")]
            return await get_weather_handler(arguments)
    except Exception as e:
        # 把错误返回给 LLM,不要崩
        return [TextContent(type="text", text=f"Error: {str(e)}")]

LLM 能看到错误 → 知道怎么调整。

2. 限速

from datetime import datetime
import collections

call_history = collections.defaultdict(list)

async def rate_limit_check(tool_name, max_per_minute=10):
    now = datetime.now()
    history = call_history[tool_name]
    # 清理 1 分钟前的记录
    history[:] = [t for t in history if (now - t).seconds < 60]
    if len(history) >= max_per_minute:
        raise Exception(f"Rate limit exceeded for {tool_name}")
    history.append(now)

3. 安全

关键工具加权限检查

ALLOWED_PATHS = ["/home/user/projects", "/tmp"]

async def file_tool_handler(arguments):
    path = arguments["path"]
    # 不允许的路径直接拒
    if not any(path.startswith(p) for p in ALLOWED_PATHS):
        return [TextContent(type="text", text="Error: path not allowed")]
    # 不允许危险操作
    if "rm -rf" in arguments.get("command", ""):
        return [TextContent(type="text", text="Error: dangerous command rejected")]
    # 执行
    ...

4. 日志

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@server.call_tool()
async def call_tool(name, arguments):
    logger.info(f"Tool called: {name} with {arguments}")
    result = await actual_handler(name, arguments)
    logger.info(f"Tool returned: {result[:200]}")
    return result

5. 配置管理

import os
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("WEATHER_API_KEY")
ALLOWED_USERS = os.getenv("ALLOWED_USERS", "").split(",")

部署

本地(Claude Desktop / Cursor)

stdio transport——本地调用:

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["server.py"],
      "env": {"WEATHER_API_KEY": "..."}
    }
  }
}

远程(HTTP)

MCP 也支持 HTTP transport:

from mcp.server.fastmcp import FastMCP

app = FastMCP("weather-tool")

@app.tool()
def get_weather(city: str) -> str:
    return f"Weather in {city}: sunny, 22°C"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

部署到 Cloudflare Workers / AWS Lambda / 自家服务器—— 让 MCP server 全球可用

发布到 MCP 生态

如果你的 server 通用—— 发布给所有人用

npm(TypeScript)

npm publish @your-org/mcp-server-weather

PyPI(Python)

pip install build twine
python -m build
twine upload dist/*

加入 MCP Servers 索引

提交 PR 到 modelcontextprotocol/servers —— 你的 server 出现在官方索引里。

全球开发者都能用

一些灵感

社区 MCP server 例子:

Server干啥
mcp-server-filesystem读写本地文件
mcp-server-gitGit 操作
mcp-server-githubGitHub API
mcp-server-postgres数据库查询
mcp-server-slackSlack 消息
mcp-server-spotify音乐控制
mcp-server-playwright浏览器自动化

几乎任何能 API 化的服务,都已经有 / 应该有 MCP server。

一些建议

做什么

问自己

  • 你公司 / 项目里 LLM 想”做”什么但做不了?
  • 你最常用的工具(Slack / Notion / GitHub)—— 有 MCP 包装吗?
  • 你的内部 tool—— 包成 MCP 后能让 AI 用吗?

做不存在的 + 通用的 —— 容易广泛使用。

起步建议

  1. 读官方文档modelcontextprotocol.io
  2. 看例子mcp servers GitHub
  3. Fork 改一改 —— 比从 0 写快
  4. 在 Claude Desktop 上测
  5. 发布 + 写文档

避免

  • 不要做权限太宽的(“任意命令执行” 危险)
  • 不要 hardcode 凭证(用 env var)
  • 不要返回大量数据(截断、分页)
  • 不要 ignore 错误(返回给 LLM 让它知道)
💡 一个商业机会

MCP 生态在 2026 还很早期—— 任何垂直领域的”MCP server”都可能成为生态地位。

  • 医疗 MCP server:连接电子病历
  • 法律 MCP server:法条 / 判例查询
  • 金融 MCP server:市场数据
  • 科研 MCP server:论文 / 数据集

做 MCP server 就像做 App Store 早期 App—— 先发优势可能巨大

下一篇推荐:L4-12 LLM 成本优化L4-13 Agent 在生产

📬

读到这里说明你认真在学 🎯

订阅每周精选 —— 下一篇新文章 / 新可视化第一时间送到邮箱。

💬

讨论区

· 用 GitHub 账号登录评论
⚠️ Giscus 评论未配置 —— 在 src/components/Comments.astro 顶部填入 仓库 ID 和分类 ID(见组件注释里的配置步骤)。