摘要从零开发一个文件查询MCP Server实现AI读取本地文件内容。涵盖路径参数设计、文件读取工具封装、安全校验和实际调用测试是MCP工具开发的实战入门项目。写一个真正有用的MCP Server 让AI查你的本地文件前三篇我们写的Server都偏演示性质算个加法阶乘没啥实际用处。这篇来个真东西一个能查本地文件的MCP Server。我之前做项目时经常需要让AI帮我翻代码库里的某个函数实现或者读某个配置文件的内容。手动复制粘贴太累于是我用MCP写了个文件搜索Server让AI自己去找、自己读。这篇我把完整实现分享出来包括路径安全校验这个重头戏。要做什么目标很明确给AI三个能力。第一按文件名模糊搜索告诉AI某个关键词它列出匹配的文件路径。第二读取指定文件的内容支持限定读取行数。第三列出某个目录下的文件和子目录。听起来简单但有个要命的安全问题。如果你不限制路径AI可能让Server读取系统敏感文件或者用../这种路径穿越跳出你设定的目录。一旦Server配在别人也能用的环境里这就是个漏洞。所以路径安全校验是这个Server的灵魂我会重点讲。设计思路我给Server设定一个允许访问的根目录所有操作都不能跳出这个根。任何传入的路径先解析成绝对路径再检查它是不是在根目录之下。不在就直接拒绝。工具设计成三个。search_files输入关键词和可选的搜索目录返回匹配的文件列表。read_file输入文件路径返回内容支持限定行数避免读超大文件。list_directory输入目录路径返回该目录下的条目。另外加一个配置限制单次读取的最大字节数和最大搜索结果数防止AI一次请求把整个磁盘读进来。路径安全校验这是全文最关键的部分单独拎出来讲。核心逻辑是这样。先定义一个允许的根目录比如C:\Users\me\projects。任何用户传入的路径先和根目录拼成绝对路径然后用Path.resolve()解析掉所有..和软链接。最后检查解析后的路径是不是根目录的子路径。frompathlibimportPath# 允许访问的根目录所有操作都被限制在这个目录内ALLOWED_ROOTPath(C:/Users/me/projects).resolve()defsafe_resolve(user_path:str)-Path|None:把用户输入的路径安全地解析成根目录内的绝对路径。 如果路径试图跳出根目录返回None表示拒绝。 # 拼接根目录和用户路径# 如果user_path本身就是绝对路径Path会忽略ALLOWED_ROOT# 所以下面要单独处理绝对路径的情况candidate(ALLOWED_ROOT/user_path).resolve()# 检查解析后的路径是否仍在根目录内# 用is_relative_to判断Python 3.9支持try:candidate.relative_to(ALLOWED_ROOT)exceptValueError:# 路径跳出了根目录拒绝returnNonereturncandidate这段代码挡住了几类攻击。一是../../../etc/passwd这种路径穿越resolve后路径会跳出根目录被拒绝。二是绝对路径注入用户传C:\Windows\System32Path(ALLOWED_ROOT / C:\Windows...)会以绝对路径为准resolve后不在根目录内被拒绝。三是软链接绕过resolve会解析软链接的真实路径指向根目录外的会被拒绝。这里有个我踩过的坑。早期版本我只用了字符串startswith来判断路径是否合法写的是str(path).startswith(str(ALLOWED_ROOT))。结果C:\Users\me\projects-evil这种路径因为前缀匹配被放行了因为它确实以projects开头。正确的做法是用Path.relative_to或者is_relative_to做路径语义级别的判断不是字符串级别的。完整代码把三个工具和安全校验组合起来完整代码如下。我加了详细注释你可以直接拿去跑。# file_server.py# 一个带路径安全校验的文件搜索MCP Server# 运行方式python file_server.py# 配合Claude Desktop或Cursor使用importsysimportloggingfrompathlibimportPathfrommcp.server.fastmcpimportFastMCP# 配置日志输出到stderr避免污染stdio协议流logging.basicConfig(levellogging.INFO,streamsys.stderr,format%(asctime)s [%(levelname)s] %(message)s,)loggerlogging.getLogger(file-server)# 允许访问的根目录改成你自己的目录# 所有文件操作都被限制在这个目录及其子目录内ALLOWED_ROOTPath(C:/Users/me/projects).resolve()# 单次读取文件的最大字节数防止读超大文件撑爆上下文MAX_READ_BYTES512*1024# 512KB# 搜索结果的最大返回数量防止匹配太多MAX_SEARCH_RESULTS50# 初始化ServermcpFastMCP(file-search)defsafe_resolve(user_path:str)-Path|None:把用户输入的路径安全地解析成根目录内的绝对路径。 返回None表示路径非法试图跳出根目录。 这是整个Server的安全核心所有工具调用前都要过这一关。 # 处理空路径默认指向根目录ifnotuser_pathoruser_path.strip():returnALLOWED_ROOT# 拼接路径并解析resolve会展开..和软链接candidate(ALLOWED_ROOT/user_path).resolve()# 校验解析后的路径是否仍在根目录内try:candidate.relative_to(ALLOWED_ROOT)exceptValueError:# 跳出根目录拒绝访问logger.warning(f拒绝访问越界路径:{user_path}-{candidate})returnNonereturncandidatemcp.tool()defsearch_files(keyword:str,sub_dir:str)-str:按文件名关键词搜索文件。 Args: keyword: 文件名关键词不区分大小写 sub_dir: 相对根目录的子目录留空则搜索整个根目录 logger.info(f搜索文件关键词{keyword}子目录{sub_dir})# 先校验搜索目录是否合法search_rootsafe_resolve(sub_dir)ifsearch_rootisNone:returnf拒绝访问子目录{sub_dir}越界# 如果指定路径本身是文件没法在其中搜索直接返回提示ifsearch_root.is_file():returnf{sub_dir}是一个文件请提供目录路径# 遍历目录树按文件名匹配keyword_lowerkeyword.lower()matches[]try:# rglob递归遍历所有文件forpathinsearch_root.rglob(*):# 只匹配文件名不匹配目录名ifpath.is_file()andkeyword_lowerinpath.name.lower():# 把绝对路径转成相对根目录的路径返回更简洁relpath.relative_to(ALLOWED_ROOT)matches.append(str(rel))# 达到上限就停止避免结果过多iflen(matches)MAX_SEARCH_RESULTS:breakexceptPermissionError:returnf没有权限访问{sub_dir}下的某些文件# 返回结果ifnotmatches:returnf没有找到包含{keyword}的文件headerf找到{len(matches)}个匹配文件最多返回{MAX_SEARCH_RESULTS}个\nreturnheader\n.join(matches)mcp.tool()defread_file(file_path:str,max_lines:int0)-str:读取指定文件的内容。 Args: file_path: 相对根目录的文件路径 max_lines: 最多读取的行数0表示不限制 logger.info(f读取文件路径{file_path}行数限制{max_lines})# 安全校验targetsafe_resolve(file_path)iftargetisNone:returnf拒绝访问路径{file_path}越界# 必须是文件ifnottarget.exists():returnf文件不存在:{file_path}ifnottarget.is_file():returnf{file_path}不是文件# 读取内容带字节数限制try:# 先按字节读超过上限就截断withopen(target,rb)asf:rawf.read(MAX_READ_BYTES1)truncatedlen(raw)MAX_READ_BYTES rawraw[:MAX_READ_BYTES]# 解码忽略无法解码的字节textraw.decode(utf-8,errorsreplace)exceptPermissionError:returnf没有权限读取{file_path}# 如果指定了行数限制截取前N行ifmax_linesandmax_lines0:linestext.splitlines()text\n.join(lines[:max_lines])# 如果被截断加个提示iftruncated:text\n\n[文件超过读取上限已截断]returntextmcp.tool()deflist_directory(dir_path:str)-str:列出指定目录下的文件和子目录。 Args: dir_path: 相对根目录的目录路径留空列出根目录 logger.info(f列出目录路径{dir_path})# 安全校验targetsafe_resolve(dir_path)iftargetisNone:returnf拒绝访问路径{dir_path}越界# 必须是目录ifnottarget.exists():returnf目录不存在:{dir_path}ifnottarget.is_dir():returnf{dir_path}不是目录# 遍历直接子项entries[]try:foriteminsorted(target.iterdir()):# 标记是文件还是目录kind目录ifitem.is_dir()else文件relitem.relative_to(ALLOWED_ROOT)entries.append(f[{kind}]{rel})exceptPermissionError:returnf没有权限访问{dir_path}ifnotentries:returnf{dir_path}是空目录headerf{dir_pathor根目录}下共{len(entries)}项\nreturnheader\n.join(entries)if__name____main__:logger.info(f文件搜索Server启动根目录{ALLOWED_ROOT})mcp.run(transportstdio)怎么用起来改两个地方就能跑。第一把ALLOWED_ROOT改成你想让AI访问的目录。第二按上一篇的方法把Server配进Claude Desktop或Cursor。配置文件这样写。{mcpServers:{file-search:{command:uv,args:[--directory,C:\\projects\\file-server,run,file_server.py]}}}配好重启客户端就可以对话了。效果验证我在项目目录里放了一些测试文件实测对话效果。问帮我找一下名字里带config的文件。AI调用search_files(keywordconfig)返回类似。找到 3 个匹配文件最多返回50个 app/config.py app/config.yaml tests/test_config.py问读一下app/config.py的前20行。AI调用read_file(file_pathapp/config.py, max_lines20)返回文件前20行内容。如果文件超过512KB末尾会出现截断提示。问列出app目录下有什么。AI调用list_directory(dir_pathapp)返回目录下的文件和子目录列表每项标注是文件还是目录。试着刁难它一下问读一下C盘Windows目录下的system32文件。AI调用read_file但因为路径越界safe_resolve返回None工具返回拒绝访问路径越界。AI拿到这个结果会告诉你说无法访问该路径。安全校验生效。常见问题与避坑坑一路径校验靠字符串匹配忽略了路径语义。上面提过startswith会把projects-evil误判为合法。一定要用Path.relative_to或is_relative_to做语义判断。这是我自己踩过并修过的真实bug。坑二没限制读取大小导致上下文爆炸。如果不设MAX_READ_BYTESAI让Server读一个10MB的日志文件全部塞进对话上下文模型token直接爆掉。解决设个合理上限比如512KB超了就截断并提示。坑三二进制文件被当文本读出乱码。用errorsreplace能避免解码崩溃但读图片、压缩包这些还是没意义。更好的做法是按扩展名过滤或者读取失败时返回提示该文件可能是二进制文件。你可以加个扩展名白名单。坑四rglob遍历超大目录树卡死Server。如果根目录下文件特别多rglob(*)会遍历很久期间Server无法响应其他请求。解决加结果数量上限提前break或者限制搜索深度用itertools.islice控制遍历量。坑五软链接绕过。如果根目录里有个软链接指向根目录外resolve()会解析到真实路径被relative_to校验拦住。但如果你用的是Path(user_path)而不resolve软链接就可能绕过校验。所以校验前一定要resolve。本文代码已经处理了这一点。和官方filesystem Server对比Anthropic官方有一个filesystem参考实现我把两者放一起对比帮你决定用哪个。对比项本文实现官方filesystem Server语言PythonTypeScript路径校验resolverelative_to类似机制模糊搜索内置关键词搜索主要是读写列举读取限制字节数行数双限制有大小限制原语类型只用ToolsTools为主上手成本一个Python文件需装Node依赖可定制性高自己改方便改源码需懂TS如果你只是要个能用的官方的装上就行。如果你想理解原理、按自己需求改本文这个Python版本更透明更好改。两者安全思路一致都是把操作限制在指定根目录内。小结一个真正有用的文件Server核心就两件事。功能上提供搜索、读取、列目录三个工具安全上用resolve加relative_to把所有路径锁死在指定根目录内。字符串匹配做路径校验是常见坑一定要用路径语义判断。读取大小和搜索数量都要设上限防止撑爆上下文或卡死Server。下一节我们把Tools、Resources、Prompts三种原语一起演示看它们各自的用法和区别。相关推荐5分钟跑通你的第一个MCP ServerPython版工具开发实战参数校验、错误处理与异步工具资源开发实战文件资源、数据库资源、动态资源