跳到主内容
快讯直播
AI智模界
教程

MCP Server 搭建入门:让 AI 调用你自己的工具

MCP(Model Context Protocol)是一套开放协议,用统一的方式把「外部能力」接给大模型应用。它把角色分成三层:Host 是用户直接用的客户端(桌面 AI 助手、IDE 插件、编辑器里的 AI 面板等),Client 是 Host 内部负责和服务器通信的连接器,Server 则是你写的那个小程序——它向模型声明「我这里有哪几个工具、哪些资源、哪些预设提示词」。通信内容是 JSON-RPC 2.0 消息,传输层常用两种:stdio(客户端把你的脚本当子进程启动,用标准输入输出收发消息,适合本地工具)和 Streamable HTTP(服务器监听端口,适合远程或多人共享)。

模型本身不会执行任何代码,它只是在对话中「决定调用哪个工具、传什么参数」,真正执行的是你的 Server。所以只要写好一个 Server,就能让 AI 读你的项目文件、查内部系统、跑脚本。下面从零搭一个能用的最小 Server。

适用场景

适合想把本地或内网的自有能力(读项目文件、查数据库、调内部 API)接给 AI 客户端的人。典型场景是:让 AI 直接扫描代码库里的 TODO、统计目录结构、读取指定文件,而不必手动复制粘贴。下面这套方案跑在个人电脑上,单机、无外网依赖,适合作为第一个 MCP Server。

环境与前置条件

  • 操作系统:macOS、Linux、Windows 均可,命令以 macOS/Linux 为主,Windows 差异处单独说明。
  • Python:3.10 及以上(具体最低版本以官方文档当前要求为准)。用 python3 --version 确认。
  • 包管理工具:推荐 uv(安装方式见官方文档),它能把依赖和运行环境一起管住,省掉虚拟环境的坑。用 pip 也可以。
  • Node.js:仅在用官方 Inspector 调试界面时需要(Inspector 通过 npx 启动),版本以官方文档当前要求为准。
  • 硬件:这类 Server 本身几乎不吃资源,512MB 内存、几百 MB 磁盘就够;真正占资源的是 AI 客户端。如果你的工具要读大文件,注意单次返回内容别太长,几万字符就足够撑爆上下文了。
  • 一个支持 MCP 的客户端:桌面 AI 助手、主流 IDE 的 AI 插件大多已支持,配置方式类似。下文以「写入客户端的 MCP 配置文件」为例,字段名不同客户端可能略有差异,以各自官方文档为准。

分步骤部署

第 1 步:建目录、装依赖

```bash

mkdir -p ~/code/mcp-demo && cd ~/code/mcp-demo

uv init --no-workspace 2>/dev/null || true

uv add "mcp[cli]"

```

uv add 会把 mcp 写进 pyproject.toml 并生成/更新虚拟环境。成功标志:命令结束无报错,目录下出现 .venv/。如果用的是 pip:

```bash

python3 -m venv .venv && source .venv/bin/activate

pip install "mcp[cli]"

```

Windows 下激活命令是 .venv\Scripts\activate

第 2 步:写最小 Server

新建 project_tools.py。这个 Server 提供三个工具(统计文件类型、搜索关键字、读取文本文件)、一个资源、一个提示词模板。资源(resource)是被动读取的数据,工具(tool)是模型可以主动调用的动作,提示词(prompt)是预置的对话模板,三者的区别在这里能直观看到。

```python

import os

from pathlib import Path

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("project-tools")

SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "dist", "build", ".idea"}

def _iter_files(root: Path):

for p in root.rglob("*"):

if p.is_file() and not any(part in SKIP_DIRS for part in p.parts):

yield p

@mcp.tool()

def count_files(root: str) -> dict:

"""统计某个目录下各类文件的数量。root 必须是绝对路径。"""

base = Path(root).expanduser().resolve()

if not base.is_dir():

raise ValueError(f"目录不存在: {base}")

result: dict[str, int] = {}

for p in _iter_files(base):

suffix = p.suffix.lower() or "(无扩展名)"

result[suffix] = result.get(suffix, 0) + 1

return dict(sorted(result.items(), key=lambda kv: kv[1], reverse=True))

@mcp.tool()

def find_todos(root: str, keyword: str = "TODO", max_results: int = 50) -> list[str]:

"""在目录下的文本文件中搜索关键字,返回「文件:行号: 内容」列表。"""

base = Path(root).expanduser().resolve()

if not base.is_dir():

raise ValueError(f"目录不存在: {base}")

hits: list[str] = []

for p in _iter_files(base):

try:

if p.stat().st_size > 2_000_000:

continue

text = p.read_text(encoding="utf-8", errors="ignore")

except OSError:

continue

for lineno, line in enumerate(text.splitlines(), 1):

if keyword in line:

hits.append(f"{p.relative_to(base)}:{lineno}: {line.strip()[:200]}")

if len(hits) >= max_results:

return hits

return hits

@mcp.tool()

def read_text_file(path: str, max_chars: int = 4000) -> str:

"""读取一个文本文件的内容,最多返回 max_chars 个字符。path 必须是绝对路径。"""

f = Path(path).expanduser().resolve()

if not f.is_file():

raise ValueError(f"文件不存在: {f}")

return f.read_text(encoding="utf-8", errors="ignore")[:max_chars]

WORKSPACE = Path(os.environ.get("MCP_WORKSPACE", ".")).expanduser().resolve()

@mcp.resource("project://files/{relpath}")

def read_resource(relpath: str) -> str:

"""把工作目录内的文件暴露为资源,URI 形如 project://files/src/main.py"""

target = (WORKSPACE / relpath).resolve()

if not str(target).startswith(str(WORKSPACE)):

raise ValueError("路径越界,已拒绝访问")

return target.read_text(encoding="utf-8", errors="ignore")[:8000]

@mcp.prompt()

def review_file(path: str) -> str:

"""生成一段代码审查提示词,并附上指定文件内容。"""

content = read_text_file(path)

return f"请审查文件 {path},指出潜在 bug、边界问题和可读性改进:\n\n``\n{content}\n``"

if __name__ == "__main__":

mcp.run()

```

几个关键点:

  • 每个工具函数的 docstring 就是给模型看的说明书,写清楚参数含义和限制,模型调用准确率会明显提高。
  • 参数和返回值都加了类型标注,SDK 会据此生成 JSON Schema 给客户端。
  • mcp.run() 默认走 stdio。想改成 HTTP 传输,用 mcp.run(transport="streamable-http"),并通过 FastMCP(...) 的参数配置监听地址和端口,具体参数名以官方文档当前版本为准。
  • 绝对不要在工具里用 print() 往 stdout 写调试信息,那会污染 JSON-RPC 消息流。

第 3 步:用 Inspector 本地调试

```bash

uv run mcp dev project_tools.py

```

这条命令会启动官方 Inspector 调试界面(通过 npx 拉起,所以需要 Node.js)。成功标志:终端打印出本地访问地址,浏览器自动打开页面。在页面里点 Connect,左侧能看到 Tools / Resources / Prompts 三栏,Tools 里列出你写的三个工具。点进 count_files,把参数填成绝对路径,运行,右侧会返回按数量排序的扩展名统计。这一步通了,说明 Server 本身没问题,剩下的都是客户端配置问题。

第 4 步:接入 AI 客户端

大多数客户端通过一个 JSON 配置文件管理 MCP Server。桌面 AI 助手的配置文件常见位置:

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

在文件里加上 mcpServers 字段(文件不存在就新建):

```json

{

"mcpServers": {

"project-tools": {

"command": "uv",

"args": [

"--directory",

"/Users/you/code/mcp-demo",

"run",

"project_tools.py"

],

"env": {

"MCP_WORKSPACE": "/Users/you/code/my-project"

}

}

}

}

```

要点:--directory 和脚本路径都写绝对路径,客户端启动子进程时的工作目录不一定是你以为的那个;env 里可以传环境变量控制 Server 行为。如果不用 uv,把 command 换成虚拟环境里的解释器绝对路径,例如 /Users/you/code/mcp-demo/.venv/bin/pythonargs 只留脚本路径。Windows 下把路径写成 C:\\Users\\you\\code\\mcp-demo\\.venv\\Scripts\\python.exe,注意反斜杠要转义。

也有客户端用 servers 而不是 mcpServers 作为字段名,改之前看一眼对应文档。部分 SDK 提供了 mcp install project_tools.py 之类的命令,可以自动写入客户端配置,具体可用性以官方文档为准。

改完保存,完全退出客户端再重新打开。

验证部署是否成功

三层验证,从内到外:

第一层,Server 能独立跑起来:

```bash

cd ~/code/mcp-demo

uv run python -c "import project_tools; print('import ok')"

```

预期输出 import ok。直接 uv run project_tools.py 会看起来「卡住」不动,这是正常的——stdio 模式下它在等客户端发消息,按 Ctrl+C 退出即可。

第二层,协议层能应答。可选的高级检查,手动喂几条 JSON-RPC 消息:

```bash

printf '%s\n' \

'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"<按官方文档填当前版本字符串>","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}' \

'{"jsonrpc":"2.0","method":"notifications/initialized"}' \

'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \

| uv run project_tools.py

```

预期能看到两行 JSON 响应,第二行里 result.tools 数组包含你定义的三个工具名。协议版本号会随协议演进变化,以官方文档当前列出的为准。

第三层,客户端里能用。重启客户端后,看输入框附近是否出现工具数量提示,或者直接在对话里问「你现在有哪些工具」。然后给一句真实指令,比如「统计 /Users/you/code/my-project 下的文件类型」,观察客户端是否弹出工具调用确认、是否返回统计结果。成功标志是模型给出的数字和你手动 find 的结果一致。

常见报错与解决

报错:客户端日志里出现 ModuleNotFoundError: No module named 'mcp'

原因:客户端启动的解释器和你装依赖的解释器不是同一个。GUI 应用继承的 PATH 往往和终端不一样。

解决:把 command 换成虚拟环境解释器的绝对路径,并去掉 uv 相关参数:

```json

"command": "/Users/you/code/mcp-demo/.venv/bin/python",

"args": ["/Users/you/code/mcp-demo/project_tools.py"]

```

报错:spawn uv ENOENTspawn python ENOENT

原因:客户端找不到可执行文件,通常是非交互式环境 PATH 不完整。

解决:用 which uv / which python3 查出绝对路径,原样写进配置;Windows 上用 where python

报错:客户端报 Unexpected tokenExpecting value: line 1 column 1

原因:Server 往 stdout 输出了非 JSON-RPC 内容,比如调试用的 print()、某个库默认往 stdout 打日志。

解决:删掉所有 print,日志改用 logging 并确保输出到 stderr(stderr 在 stdio 模式下是安全的):

```python

import logging, sys

logging.basicConfig(stream=sys.stderr, level=logging.INFO)

```

报错:工具返回「目录不存在」或读取到意外的文件

原因:相对路径的基准是客户端进程的工作目录,不是你的项目目录。

解决:所有路径参数一律传绝对路径;在 Server 内部也用 Path(...).resolve() 兜底,必要时加一层路径白名单校验(示例代码里的 project:// 资源就是这么做的)。

报错:首次调用就报 Connection closed

原因:脚本顶层有异常,进程启动后立刻退出。

解决:先在终端手动运行脚本看完整 traceback,确认 uv run python project_tools.py 不报错再交给客户端;同时检查脚本里没有把 mcp.run() 写在 if __name__ == "__main__": 之外导致被重复执行。

后续维护

备份:把两样东西纳入版本管理——Server 代码,以及客户端的 MCP 配置文件(复制一份到项目里,注意别把密钥提交上去)。改配置前先备份原文件,JSON 语法错误会让客户端直接不加载。

升级:SDK 升级用 uv add "mcp[cli]"(或 pip install -U "mcp[cli]")重新解析依赖,升级后先跑一遍 mcp dev 验证工具都还在,再重启客户端。协议会演进,客户端与 SDK 版本跨得太远时可能出现字段不兼容,升级前看一眼官方文档的变更说明。

日志:HTTP 传输时把 stderr 重定向到文件,例如 uv run project_tools.py 2>> ~/logs/mcp-tools.log,方便事后排查。stdout 永远留给协议本身。

安全:Server 等于给 AI 开了一扇窗。原则是只暴露必需的目录和接口,默认只读;涉及写文件、删数据、发请求这类操作,要么不做,要么在客户端开启逐次人工确认。远程部署时务必加上认证和 TLS,别把无鉴权的 MCP 端口直接暴露到公网。

迭代:工具变复杂后,建议每个工具只做一件事,参数尽量扁平,docstring 写清楚输入输出示例。模型调用出错时,先怀疑描述写得不够明确,再怀疑代码有 bug——多数情况是前者。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。