本文仅用于本地开发学习,MCP服务拥有本地文件读写权限,存在敏感文件泄露、路径穿越风险,请严格遵守以下安全规范:
一、前言
日常使用Cursor、Claude Desktop、VS Code MCP客户端时,AI只能处理对话内粘贴的代码片段,无法直接读取本地项目目录、批量检索源码、统计文件结构、预览日志文件。
核心原因:AI客户端默认无本地文件系统操作权限,而 MCP(Model Context Protocol 模型上下文协议) 是Anthropic推出的标准化AI本地工具通信协议,相当于AI的通用USB接口,一次开发MCP服务,所有支持MCP的AI客户端均可无缝调用本地文件能力。
本文基于Python FastMCP SDK,从零实现文件搜索、目录统计、文件预览三大工具,完整兼容Cursor、Claude Desktop,附带安全权限校验、客户端全平台配置教程。
适用场景
- 本地项目批量检索py/json配置文件
- 代码目录结构统计、文件大小汇总
- 日志、源码快速预览,无需手动打开文件
- AI自动分析本地项目工程结构
二、MCP协议通俗原理
把MCP理解为AI客户端的标准化外设接口:
| 电脑主机 | Claude/Cursor/VS Code AI客户端 |
| U盘/外设 | 自定义Python本地工具服务 |
| USB通信标准 | MCP(JSON-RPC/Streamable HTTP/stdio) |
| 即插即用配置 | 客户端mcpServers配置项 |
传统方案:不同AI工具需要单独开发插件、适配各自私有API;
MCP标准化后:一套MCP服务,全平台AI客户端通用,大幅降低本地工具开发成本。
三、环境部署
1. Python版本要求
Python ≥ 3.10
# 查看版本
python3 –version
2. 安装依赖库
# MCP官方SDK + HTTP服务框架
pip install mcp fastapi uvicorn
依赖说明:
- mcp:FastMCP官方Python开发库,快速注册AI可调用工具
- fastapi+uvicorn:提供Streamable HTTP服务传输通道
3. 项目初始化
# 创建项目文件夹
mkdir -p ~/my-file-mcp && cd ~/my-file-mcp
四、完整MCP服务代码(带安全白名单+详细注释)
新建 file_scanner.py,增加目录白名单防护,规避路径穿越漏洞:
"""
本地文件扫描MCP Server(安全加固版)
功能:文件检索、目录统计、文件内容预览
安全限制:仅允许访问预设白名单目录,防止路径穿越攻击
"""
import os
from pathlib import Path
from mcp.server.fastmcp import FastMCP
# ====================== 安全配置:允许访问的目录白名单(自行修改) ======================
ALLOW_DIRS = [
Path("~/projects").expanduser(),
Path("~/code").expanduser()
]
# 转换为绝对路径,用于校验
ALLOW_ABS = [p.resolve() for p in ALLOW_DIRS]
# 初始化MCP服务
mcp = FastMCP(
"File Scanner",
description="本地文件系统安全扫描工具,仅读取白名单内目录,支持文件检索、目录统计、源码预览",
port=8765
)
def is_path_safe(target_path: str) –> bool:
"""路径安全校验:仅允许访问白名单目录,拦截../路径穿越"""
target = Path(target_path).resolve()
# 判断目标路径是否在任一允许目录下
for safe_dir in ALLOW_ABS:
if str(target).startswith(str(safe_dir)):
return True
return False
@mcp.tool()
def search_files(directory: str, pattern: str = "*.py", max_depth: int = 3) –> str:
"""
递归搜索指定目录下匹配后缀的文件
Args:
directory: 目标检索目录(必须在白名单内)
pattern: 文件匹配规则,如 *.py、*.json
max_depth: 最大递归深度,避免遍历过多文件
Returns:
匹配到的文件路径列表
"""
# 安全校验
if not is_path_safe(directory):
return f"权限拒绝:{directory} 不在允许访问目录列表中,请修改ALLOW_DIRS配置"
base = Path(directory)
if not base.exists():
return f"错误:目录不存在 —— {directory}"
results = []
try:
for depth in range(max_depth + 1):
glob_rule = "**/*" + pattern if depth > 0 else pattern
for file_path in base.glob(glob_rule):
if file_path.is_file() and len(results) < 50:
results.append(str(file_path))
except Exception as e:
return f"文件搜索异常:{str(e)}"
if not results:
return f"在 {directory} 未匹配到 {pattern} 文件(最大深度{max_depth})"
return f"共找到 {len(results)} 个文件:\\n" + "\\n".join(results)
@mcp.tool()
def dir_stats(path: str) –> str:
"""
统计目录:文件总数、总大小、文件类型分布
Args:
path: 目标统计目录
Returns:
结构化目录统计报告
"""
if not is_path_safe(path):
return f"权限拒绝:{path} 不在允许访问目录列表中"
p = Path(path)
if not p.exists():
return f"错误:路径不存在 —— {path}"
total_files = 0
total_size = 0
ext_count = {}
for item in p.rglob("*"):
if item.is_file():
total_files += 1
size = item.stat().st_size
total_size += size
suffix = item.suffix if item.suffix else "(无后缀)"
ext_count[suffix] = ext_count.get(suffix, 0) + 1
# 按文件数量降序排序
sorted_ext = sorted(ext_count.items(), key=lambda x: –x[1])
lines = [
f"📁 统计目录:{path}",
f"📄 文件总数量:{total_files}",
f"💾 目录总大小:{total_size / 1024:.1f} KB",
"",
"前10类文件分布:"
]
for ext, num in sorted_ext[:10]:
lines.append(f" {ext}:{num} 个")
return "\\n".join(lines)
@mcp.tool()
def preview_file(path: str, max_lines: int = 30) –> str:
"""
预览文件前N行内容,避免一次性读取超大文件占满上下文
Args:
path: 文件完整路径
max_lines: 最多预览行数,默认30行
Returns:
文件元信息 + 头部内容预览
"""
if not is_path_safe(path):
return f"权限拒绝:{path} 不在允许访问目录列表中"
p = Path(path)
if not p.exists() or not p.is_file():
return f"错误:文件不存在或非文件 —— {path}"
try:
with open(p, "r", encoding="utf-8", errors="replace") as f:
all_lines = f.readlines()
preview = all_lines[:max_lines]
except Exception as e:
return f"文件读取失败:{str(e)}"
file_size = p.stat().st_size
total_line_num = len(all_lines)
header = (
f"📄 文件路径:{path}\\n"
f"文件大小:{file_size/1024:.1f} KB | 总行数:{total_line_num}\\n"
f"预览前 {min(max_lines, total_line_num)} 行内容:\\n\\n"
)
return header + "".join(preview)
if __name__ == "__main__":
# Streamable HTTP 本地服务(仅localhost访问)
mcp.run(transport="streamable-http")
启动服务命令
python file_scanner.py
正常输出示例:
INFO: Uvicorn running on http://0.0.0.0:8765
INFO: Application startup complete.
五、AI客户端接入配置(区分Mac/Windows)
5.1 Claude Desktop 配置
配置JSON(stdio传输模式,推荐本地使用):
{
"mcpServers": {
"file-scanner": {
"command": "python",
"args": ["~/my-file-mcp/file_scanner.py"],
"transport": "stdio"
}
}
}
修改完成后重启Claude Desktop,自动加载MCP服务。
5.2 Cursor 编辑器配置
| 配置项 | 填写内容 |
| Name | file-scanner |
| Command | python |
| Arguments | ~/my-file-mcp/file_scanner.py |
| Transport | stdio |
保存后刷新MCP列表,显示服务在线即连接成功。
六、实操调用示例
客户端连接成功后,直接对话发送指令即可自动调用文件工具:
调用file-scanner,扫描~/projects目录,找出所有*.py文件,最大深度3
统计~/projects目录总文件数量、总大小,展示各类文件占比
读取~/projects/utils.py前20行代码并展示文件信息
七、进阶扩展工具模板
基于@mcp.tool()装饰器可快速扩展更多本地自动化能力,推荐开发:
| git_log | 读取Git仓库最近提交记录 |
| log_tail | 实时读取日志末尾内容 |
| dir_diff | 对比两个目录文件差异 |
| docker_list | 列出本地运行容器 |
| cron_check | 读取系统定时任务配置 |
新增工具仅需定义函数、增加参数注解,MCP会自动生成工具描述供AI识别。
八、安全加固&生产优化方案
九、常见踩坑FAQ
Q1:客户端识别不到MCP服务?
A:检查Python路径是否正确、配置文件路径区分Mac/Windows,修改配置后重启客户端;确认mcp库版本≥1.0。
Q2:调用文件工具提示权限拒绝?
A:将目标目录添加到代码顶部ALLOW_DIRS白名单,重启MCP服务生效。
Q3:启动服务报端口占用?
A:修改FastMCP初始化时port=xxxx更换端口,同步修改客户端配置。
Q4:读取中文文件乱码?
A:代码已使用encoding="utf-8", errors="replace",特殊编码文件可自行调整编码参数。
十、全文总结
本教程基于Python MCP SDK v1.x编写,代码可直接本地运行,仅用于个人开发学习。
网硕互联帮助中心






评论前必须登录!
注册