MCP 进阶实战:自建一个 MCP Server

本篇假设你已经读过站内「mcp-protocol 基础篇」,了解 MCP 是什么、为什么出现。这里我们直接进入动手环节:用官方 SDK 写一个能跑的 MCP Server,把它接入客户端,并聊聊调试与安全那些坑。

一、回顾:MCP 到底解决了什么问题

在 MCP 出现之前,每增加一个大模型应用(Host),每接一个外部工具或数据源(日历、数据库、天气、文件系统……),开发者都得写一套专属对接代码。N 个应用乘 M 个工具,就是 N×M 套集成,重复、脆弱、难以维护。

MCP 把这件事标准化了:它定义了一套开放协议,让工具和数据源以统一方式对外暴露能力,任何兼容 MCP 的客户端都能即插即用。于是集成从「N×M」收敛成「N+M」——你只要把工具写成一个 MCP Server,所有支持 MCP 的客户端都能复用;反过来,客户端也能同时接多个 Server。

本篇不重复讲协议原理,只聚焦一件事:如何亲手造一个这样的 Server。

二、MCP Server 的骨架:三个原语

官方文档(已核验)明确,一个 MCP Server 主要可以对外提供三类能力,称为原语:

  • Resources(资源):类文件的数据,客户端可以读取,例如某个 API 的响应、某份文件的内容。
  • Tools(工具):可以由大模型调用的函数,通常需要用户批准后执行。
  • Prompts(提示词):预写好的模板,帮助用户完成特定任务。

本篇重点放在 Tools 上,因为它是让大模型「动手」的关键。下面分别用官方 Python SDK 和 TypeScript SDK 看骨架长什么样。

Python(官方 mcp 库,v2 风格,接口细节待核实):

from mcp.server import MCPServer

mcp = MCPServer("Demo")


@mcp.tool()
def add(a: int, b: int) -> int:
    """把两个整数相加。"""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """按名字生成一句问候。"""
    return f"你好,{name}!"

TypeScript(官方 SDK):

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

const server = new McpServer({ name: "Demo", version: "1.0.0" });

server.registerTool(
  "add",
  { description: "把两个整数相加。", inputSchema: { a: z.number(), b: z.number() } },
  async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] })
);

const transport = new StdioServerTransport();
await server.connect(transport);

注意一个关键点:Python v2 SDK 用类型注解自动推导出输入结构,不用手写 JSON Schema;函数文档字符串会作为工具描述暴露给模型。这正是「少写样板」的核心。

三、实操:写一个可运行的「查询天气」工具

下面这段 Python 可以直接运行(依赖官方 mcp 库)。它把「按经纬度查天气」封装成一个 tool,底层调用免费的 Open-Meteo 接口,无需申请密钥。

import json
import urllib.request
from mcp.server import MCPServer

mcp = MCPServer("weather-demo")


@mcp.tool()
def get_forecast(latitude: float, longitude: float) -> str:
    """根据经纬度查询未来若干小时的天气。

    latitude: 纬度,例如 39.9 表示北京附近。
    longitude: 经度,例如 116.4 表示北京附近。
    """
    url = (
        "https://api.open-meteo.com/v1/forecast"
        f"?latitude={latitude}&longitude={longitude}&hourly=temperature_2m"
    )
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except Exception as exc:  # 网络异常时返回可读错误,而非崩溃
        return f"查询失败:{exc}"
    return json.dumps(data.get("hourly", {}), ensure_ascii=False)


if __name__ == "__main__":
    # 默认以 stdio 传输启动。具体方法签名(如是否需传 transport 参数)待核实。
    mcp.run()

运行前先安装依赖:

pip install "mcp[cli]"

本地调试推荐用官方 Inspector(已核验命令):

mcp dev server.py

它会启动一个调试页面,你能直接看到工具列表、手动发起调用,确认 Server 行为符合预期。

要点回顾:

  • 工具函数名 get_forecast 会成为工具标识。
  • 类型注解 float 自动成为参数 schema。
  • 文档字符串会被模型看到,写清楚、写准确能显著提升调用正确率。
  • try/except 兜底外部请求失败,避免 Server 直接崩掉。

四、接入客户端:在 Claude Desktop 中配置

要让客户端真正用上这个 Server,需要在客户端配置里登记它。以 Claude Desktop 为例(配置结构与路径已核验):

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

把下面内容写入(或合并进)该 JSON。注意把路径换成你真实的 server.py 所在目录:

{
  "mcpServers": {
    "weather-demo": {
      "command": "uv",
      "args": [
        "--directory",
        "C:/your/path/to/server",
        "run",
        "python",
        "server.py"
      ]
    }
  }
}

字段含义:

  • weather-demo:给这个 Server 起的名字,会显示在客户端里。
  • command:启动命令,这里用 uv 来运行(你也可以用 python 直接跑,但需保证环境里已装好 mcp)。
  • args:传给命令的参数,核心是定位并运行 server.py
  • 如果工具依赖密钥等环境变量,可额外加 env 字段,例如 {"env": {"SOME_API_KEY": "..."}}(格式已核验)。

保存后完全退出并重启 Claude Desktop,在输入框左下角的「添加文件、连接器……」入口里找到对应连接器,即可看到 get_forecast 工具。大模型在需要时就会请求调用它,并等你批准。

五、调试与安全:传输、鉴权与危险操作

传输方式怎么选

MCP 官方 SDK 支持三类传输(已核验):

  • stdio(标准输入/输出):最常用,客户端以子进程方式拉起 Server,通过 stdin/stdout 收发 JSON-RPC。适合本地、单机场景。
  • Streamable HTTP:Server 以网络服务形式常驻,客户端用 URL 访问。适合远程部署、多客户端共享。
  • SSE(Server-Sent Events):另一种基于 HTTP 的流式传输,老方案,逐渐被 Streamable HTTP 取代。

用 CLI 把同一个 Server 跑成 HTTP 服务(已核验):

uv run mcp run server.py --transport streamable-http

客户端侧一个 URL 即代表 Streamable HTTP 传输,例如 http://localhost:8000/mcp

stdio 的一个致命细节

官方文档明确警告(已核验):stdio 模式的 Server 绝不能往 stdout 写任何东西,包括 print()stdout 是协议通道,你写进去的内容会和 JSON-RPC 消息混在一起,直接把通信搞坏。调试信息请写 stderr,或接入 SDK 自带的日志机制。HTTP 模式则不受影响,可以正常写标准输出。

鉴权

本地 stdio Server 通常跟随你的用户权限运行,本身不额外鉴权。但一旦用 Streamable HTTP 暴露到网络,就必须自己加防护:用 API Key、OAuth 或反向代理(如 Nginx)做访问控制,别把无鉴权的工具服务裸奔在公网。涉及密钥的配置放在 env 里,不要硬编码进代码或提交到仓库。

不要把危险操作无防护暴露

这是最重要的一条。MCP 让大模型能「自动执行函数」,意味着你暴露的每一个 tool 都可能被模型在用户批准后真实运行。请遵守:

  • 不要把「删除文件」「执行任意 shell」「改写数据库」这类高危操作直接暴露成 tool,至少加上白名单、路径限制和二次确认。
  • 对外暴露本地文件读取时,明确限定允许的根目录,避免路径穿越读到系统敏感文件。
  • 工具描述要诚实,别让模型误以为某个只读工具其实会改数据。
  • 涉及写操作、外部请求的工具,做好输入校验和异常兜底(如第三节的 try/except)。

一句话原则:能跑代码的接口,就等于把一部分机器控制权交了出去。暴露之前先想清楚「最坏情况下谁会受伤」。

小结

  • MCP 把「N×M」的工具集成收敛成「N+M」:写一次 Server,所有兼容客户端复用。
  • Server 通过 Tools / Resources / Prompts 三类原语对外暴露能力,Tools 是让模型「动手」的关键。
  • 用官方 mcp 库,类型注解即 schema,文档字符串即工具描述,几乎不用写样板。
  • 接入客户端靠一份 mcpServers 配置;stdio 模式严禁写 stdout,HTTP 模式记得加鉴权。
  • 暴露工具等于交出部分执行权,高危操作务必加防护、加确认、加白名单。

参考与延伸阅读

  • MCP 官方文档「Build an MCP server」:核心原语(Tools/Resources/Prompts)与传输说明。已核验
  • MCP Python SDK 仓库(modelcontextprotocol/python-sdk):MCPServer@mcp.tool()@mcp.resource() 用法,v2 当前为稳定线,pip install mcp 默认装 2.x。已核验
  • MCP 官方「Connect to local MCP servers」:Claude Desktop 的 claude_desktop_config.json 结构与路径(Windows %APPDATA%\Claude\、macOS ~/Library/Application Support/Claude\)。已核验
  • 官方调试指南(debugging):Inspector 与日志排查。详见 MCP 文档站。已核验

待核实项:

  • Python SDK 的具体版本号(仓库仅标注为 v2 稳定线,未给出精确版本号,例如 2.x.y)。待核实
  • MCPServer.run() 在 stdio 下的精确方法签名(是否需传 transport="stdio" 等参数)。待核实
  • 旧版 FastMCP 在 v2 中是否保留或完全移除;若你仍用 v1,需约束 mcp>=1.28,<2 并改用 FastMCP 写法。待核实
  • 文中天气示例使用的 Open-Meteo 接口地址与返回字段可能随服务端更新变化。待核实
本文累计阅读