当前位置:首页 > 编程开发 > 正文

从零写一个 MCP 服务器:让你的脚本变成 AI 能直接调的工具

约 5557 字 · 预计阅读 14 分钟
一句话总结:MCP 解决的是「AI 想用你的工具,但不知道它存在」这个问题。这篇从 20 行的最小服务器写起,讲清 tool / resource / prompt 三种原语的分工、参数设计为什么比代码重要、怎么接进 Claude Code 和 WorkBuddy,以及 stdio 模式下最容易踩的那个坑。

先说痛点。你有个查日志的脚本、一个内部 API、一个只读数据库连接。每次让 AI 帮忙分析,都得手动把结果复制过去。MCP 就是干这个的:把你的能力包成标准接口,客户端一配置,模型就能自己决定什么时候调、传什么参数。

它本质是 JSON-RPC,传输层可以是 stdio(本地进程)或 HTTP。对个人用户,stdio 最简单——客户端启动一个子进程,通过标准输入输出通信。

最小可跑版本

装 SDK:

pip install mcp
# 或者用 uv(推荐,隔离干净)
uv add mcp

二十行写出第一个服务器:

from mcp.server.FastMCP import FastMCP

mcp = FastMCP("demo-tools")

@mcp.tool()
def count_lines(path: str) -> int:
    """统计文本文件的行数。

    Args:
        path: 文件绝对路径
    """
    with open(path, encoding="utf-8", errors="ignore") as f:
        return sum(1 for _ in f)

if __name__ == "__main__":
    mcp.run()   # 默认 stdio

就这些。@mcp.tool() 装饰器把函数注册成工具,参数类型注解自动生成 JSON schema,docstring 变成工具描述。

跑起来是阻塞的,终端看着像卡住——正常,它在等客户端说话。

三种原语,别混用

MCP 给了三个概念,用途完全不同:

原语 语义 谁来触发 典型用途
Tool有副作用的动作模型决定调用查数据库、发请求、写文件
Resource只读数据客户端按 URI 读取配置文件、文档、日志片段
Prompt可复用模板用户主动选择固定的分析流程、代码审查清单

新手最常见的错误是把所有东西都做成 tool。判断很简单:只读、且有固定地址的数据,做成 resource;需要参数组合、会改变状态的动作,才做成 tool。如果你只是想给本地模型接几个工具、不打算搭通用协议,llama.cpp 的 Function Calling那条路更轻。

Resource 的写法:

@mcp.resource("config://app")
def get_config() -> str:
    """当前应用的配置(只读)"""
    return open("app.toml", encoding="utf-8").read()

好处是这类数据不消耗工具调用的轮次,客户端可以直接把它塞进上下文。

参数设计比代码重要

这段是我认为整篇最值钱的部分。

模型决定要不要调你的工具,靠的是工具名 + 描述 + 参数 schema。代码写得再漂亮,描述写得含糊,它就是不会调。

三条实操建议:

一、工具名用动词开头,写清对象。 query_slow_logs 比 logs 好,get_user_orders 比 orders_query 好。

二、描述里写「什么时候用」和「什么时候别用」。 只写「查询日志」不够,要写「用于按时间范围和级别筛选服务端日志;不用于查询业务数据,那用 query_orders」。

三、参数用具体类型,别用裸 dict。 类型注解越具体,schema 越清晰,模型填错参数的概率越低。

from typing import Literal

@mcp.tool()
def query_slow_logs(
    service: str,
    minutes: int = 30,
    level: Literal["warn", "error", "fatal"] = "error",
    liMIT: int = 50,
) -> str:
    """查询指定服务最近的慢请求与错误日志。

    用于排查线上问题时获取原始日志。想看业务数据请用 query_orders。

    Args:
        service: 服务名,如 order-api、user-api
        minutes: 回溯分钟数,默认 30,上限 1440
        level: 最低日志级别
        limit: 最多返回条数,默认 50,上限 200
    """

Literal 会让 schema 变成枚举,模型基本不会填错。默认值和上限写在 docstring 里,模型会读到。

返回值建议统一成字符串或可 JSON 序列化的结构。别直接返回 ORM 对象,序列化失败会让整个调用报错,而模型只会看到「工具调用失败」这种无用信息。

接进客户端

MCP 服务器的配置就是一个命令加参数。各家客户端的配置文件位置不同,结构几乎一样。

WorkBuddy 是 ~/.workbuddy/mcp.json:

{
  "mcpServers": {
    "demo-tools": {
      "command": "C:/path/to/Python.exe",
      "args": ["D:/projects/demo_mcp/server.py"],
      "env": { "LOG_LEVEL": "info" }
    }
  }
}

Claude Code 可以用命令加,也可以写 .mcp.json:

claude mcp add demo-tools -- C:/path/to/python.exe D:/projects/demo_mcp/server.py

两个细节:路径写绝对路径(客户端的工作目录不确定),Python 解释器写全路径(别指望客户端继承你的 PATH)。

Windows 上还有个隐藏问题:如果用的是微软商店版 Python,python.exe 是个占位符,被调用时会静默失败。用 where python 确认一下真实路径。

图 1:客户端与服务器之间是 JSON-RPC,服务器再调用你的实际工具

那个必踩的坑:stdout 被污染

stdio 模式下,标准输出就是协议通道。你在工具函数里随手 print("debug"),那一行会插进 JSON-RPC 流里,客户端解析失败,表现为「工具调用超时」或者「服务器无响应」。

排查起来极其费时间,因为服务器自己看着一切正常。

规矩很简单:

import sys

def log(msg):
    print(msg, file=sys.stderr)   # 日志一律走 stderr

或者干脆写文件。第三方库的日志也要管住——有些库默认往 stdout 打,得手动把它重定向到 stderr。

这一条我踩过,找了半小时才发现是某个 SDK 的 banner 打在了 stdout。

坑清单

坑一:stdout 混入非协议内容。 上面说过,排第一。任何 print、任何库的 banner、任何进度条都会破坏协议。

坑二:工具函数抛未捕获异常。 服务器可能整个挂掉,而不是返回一条错误信息。用 try/except 包住,把错误当返回值传回去,模型能读到原因,还能自己重试。

坑三:一次注册太多工具。 三十个工具塞给模型,它会挑错。分组做多个服务器,按场景启用,比一个全能服务器好用。

坑四:没有超时和上限。 一个查询跑十分钟,客户端早超时了。所有工具都要有内部超时和结果条数上限,参数里也要暴露给模型。

坑五:把写操作做成默认行为。 删除、覆盖、发消息这类动作,参数里加个 confirm: bool 或者干脆只提供 dry-run,让人看一眼再执行。

边界

stdio 服务器跑在你自己的机器上、你自己的权限下。工具能读什么,取决于你的进程权限,MCP 本身不做沙箱。所以别把「删库」这种能力包成工具交给模型,除非你有确认环节。

另外提醒一句:第三方 MCP 服务器要当第三方代码看。它能在你的机器上执行任意操作,装之前看一下源码或者至少确认来源。

FAQ

必须用 Python 吗?

不用。官方 SDK 有 Python、TypeScript,社区还有 Go、Rust、Java 实现。选你手边最熟的语言,协议层是一样的。

stdio 和 HTTP 怎么选?

本地工具用 stdio(简单、无需端口);要给多个客户端共享或者跨机器,才用 HTTP + 鉴权。

一个服务器能同时提供 tool 和 resource 吗?

能,同一个 mcp 实例上两种装饰器可以混用,客户端会分别列出。

怎么知道模型到底调没调我的工具?

看客户端的调用日志(大多会显示工具名和参数),服务器这边用 stderr 打点。两边对一下就知道是「没调」还是「调了失败」。

写好的服务器能发布吗?

可以,但公开的服务器要考虑鉴权、限流和输入校验。私用和个人项目直接本地跑就行。

结语

MCP 的价值不在协议本身,在于它把「给 AI 加一个能力」的成本降到了写一个函数加一个装饰器。

真要上手,从最小那个 20 行版本开始,先跑通再加功能。别一上来设计十个工具——你会发现模型只用其中三个,而且不是你最看好的那三个。

参考资料

目录导航
  • 最小可跑版本
  • 三种原语,别混用
  • 参数设计比代码重要
  • 接进客户端
  • 那个必踩的坑:stdout 被污染
  • 坑清单
  • 边界
  • FAQ
  • 结语
    • 参考资料
  • 相关文章:

  • RAG 实战:几百行代码搭一个自己的知识库问答(Python + SQLite,不碰框架)2026-10-27 14:16:47
  • MCP 协议深入:Tools、Resources、Prompts 三种原语怎么设计才不翻车2026-10-27 14:16:47
  • MCP Server 开发实战:从零用 Python 给自己写一个 AI 工具,附完整代码与避坑清单2026-10-27 14:16:47
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。