Claude 一旦不再只是个聊天界面,就会变得实用得多。
本地 MCP 服务器可以让 Claude 与你的实际机器交互:本地文件、获批命令、截图以及应用启动。关键不在于原始权限,而在于可控的访问权限。
在本指南中,我们将为 Claude 构建一个本地 MCP 服务器,它实用、专注且足够安全,可用于实际工作流。
开始构建之前的一点说明
本文特意避开了那种不负责任的“电脑控制”版本。
我们不会给 Claude 无限制的 Shell 权限、完整的文件系统权限,或者在没有防护措施的情况下操作你的机器。构建糟糕的本地 MCP 服务器最快的方式,就是暴露一个巨大的 run_anything() 工具,然后美其名曰创新。
更好的模式是:
- 白名单目录
- 白名单命令
- 安全默认值
- 人类可读的日志
- 明确的响应
- 只读工具与操作工具之间清晰分离
如果 Claude 什么都能做,那你只是建了个演示版。
如果 Claude 能安全地做正确的事,那你才真正构建了可用的东西。
为什么这套架构值得学习
本地 MCP 服务器的价值不在于新奇,而在于减少摩擦。
没有本地工具层,你的工作流会是这样:
- 问 Claude 该怎么做
- 复制答案
- 自己打开文件夹
- 自己运行命令
- 自己截图
- 把结果粘贴回聊天
有了本地 MCP 服务器,这个循环会大大缩短。Claude 可以检查它需要的上下文,使用范围狭窄的工具,然后返回一个基于你实际机器状态的答案。
这在以下场景中非常有用:
- 开发工作流
- 日志检查
- 内容运营
- 研究管线
- 桌面自动化
- 可重复的管理任务
而且因为工具层属于你,你可以精确决定模型在何处停止。
想学习 AI 并在职场中保持领先?
我分享:
1000+ 个 AI 提示词
AI 工具与自动化
职场与 LinkedIn 成长技巧
精选的技术与生产力资源
在此订阅:
关注我获取更多 AI 技巧
👇
我们要构建的设计

我们将构建一个包含五个工具的本地服务器:
- list_files — 查看已批准文件夹内的内容
- read_file — 打开安全的基于文本的文件
- run_command — 执行一小套获批的本地命令
- take_screenshot — 将截图保存到已知位置
- open_target — 打开应用、文件、文件夹或 URL

这个范围是经过深思熟虑的。
它足以让 Claude 在本地机器上发挥真正的作用,同时又不会陷入不安全的通用自动化。
心理模型应该是这样的:
Claude → MCP 客户端 → 本地 MCP 服务器 → 狭窄的工具 → 操作系统
Claude 永远不应该直接与你的操作系统对话。你的 MCP 服务器就是中间的控制平面。
技术栈
对于本地构建,Python 是一个简洁的选择,因为官方的 MCP SDK 已经成熟,FastMCP 抽象很简洁,而且 Python 仍然是文件系统、子进程和桌面脚本编程最容易的语言 4 2。
我们将使用:
- Python 3.11+
- mcp[cli] 用于 MCP 服务器运行时
- mss 用于跨平台截图
- 标准库模块用于文件访问、子进程调用和操作系统处理
设置一个新项目:
1mkdir local-mcp-server2cd local-mcp-server3uv init --python 3.114uv add "mcp[cli]>=1.0,<2.0" "mss>=9.0,<10.0"
基于装饰器的 FastMCP 风格让协议层不会妨碍你,这样你可以专注于工具质量,而不是连接工作 4 5。
一个简单的项目布局就很好:
1mkdir local-mcp-server2cd local-mcp-server3uv init --python 3.114uv add "mcp[cli]>=1.0,<2.0" "mss>=9.0,<10.0"
v1 版本不需要复杂的架构。你需要的是清晰。
实际的服务器
创建 server.py 并以策略驱动的实现开始。
1from __future__ import annotations23import json4import os5import platform6import shlex7import subprocess8from pathlib import Path9from typing import Any1011import mss12from mcp.server.fastmcp import FastMCP1314app = FastMCP("local-computer-control", json_response=True)1516HOME = Path.home()17PROJECT_ROOT = Path(__file__).parent.resolve()18CAPTURE_DIR = PROJECT_ROOT / "captures"19CAPTURE_DIR.mkdir(exist_ok=True)2021ALLOWED_ROOTS = [22 HOME / "Documents",23 HOME / "Desktop",24 PROJECT_ROOT,25]2627ALLOWED_COMMANDS = {28 "pwd",29 "ls",30 "git status",31 "git diff --stat",32 "python --version",33 "node --version",34 "npm --version",35}3637READABLE_EXTENSIONS = {38 ".txt",39 ".md",40 ".json",41 ".py",42 ".js",43 ".ts",44 ".tsx",45 ".jsx",46 ".yaml",47 ".yml",48 ".toml",49 ".csv",50 ".log",51}5253def _resolve_path(raw_path: str) -> Path:54 path = Path(raw_path).expanduser().resolve()55 for root in ALLOWED_ROOTS:56 root = root.resolve()57 if path == root or root in path.parents:58 return path59 raise ValueError(f"路径不被允许: {path}")6061def _ensure_safe_command(command: str) -> str:62 normalized = " ".join(shlex.split(command))63 if normalized not in ALLOWED_COMMANDS:64 raise ValueError(65 "命令不被允许。如果确实需要,请将其显式添加到 ALLOWED_COMMANDS 中。"66 )67 return normalized6869@app.tool()70def list_files(path: str = "~") -> dict[str, Any]:71 """列出已批准目录内的文件和文件夹。"""72 target = _resolve_path(path)73 if not target.is_dir():74 raise ValueError(f"不是目录: {target}")7576 items = []77 for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):78 items.append(79 {80 "name": child.name,81 "path": str(child),82 "type": "directory" if child.is_dir() else "file",83 }84 )8586 return {87 "path": str(target),88 "count": len(items),89 "items": items,90 }9192@app.tool()93def read_file(path: str, max_chars: int = 12000) -> dict[str, Any]:94 """从已批准位置读取安全的文本文件。"""95 target = _resolve_path(path)96 if not target.is_file():97 raise ValueError(f"不是文件: {target}")98 if target.suffix.lower() not in READABLE_EXTENSIONS:99 raise ValueError(f"不支持的文件类型: {target.suffix}")100101 content = target.read_text(encoding="utf-8", errors="replace")102 truncated = len(content) > max_chars103 content = content[:max_chars]104105 return {106 "path": str(target),107 "truncated": truncated,108 "content": content,109 }110111@app.tool()112def run_command(command: str, cwd: str | None = None, timeout: int = 15) -> dict[str, Any]:113 """运行一条白名单中的本地命令。"""114 safe_command = _ensure_safe_command(command)115 working_dir = _resolve_path(cwd) if cwd else PROJECT_ROOT116117 completed = subprocess.run(118 safe_command,119 shell=True,120 cwd=str(working_dir),121 capture_output=True,122 text=True,123 timeout=timeout,124 )125126 return {127 "command": safe_command,128 "cwd": str(working_dir),129 "returncode": completed.returncode,130 "stdout": completed.stdout.strip(),131 "stderr": completed.stderr.strip(),132 }133134@app.tool()135def take_screenshot(name: str = "latest") -> dict[str, Any]:136 """截图并保存到本地。"""137 output_path = CAPTURE_DIR / f"{name}.png"138139 with mss.mss() as sct:140 sct.shot(output=str(output_path))141142 return {143 "saved": True,144 "path": str(output_path),145 }146147@app.tool()148def open_target(target: str) -> dict[str, Any]:149 """使用本地操作系统打开已批准的文件、文件夹、应用或 URL。"""150 system = platform.system().lower()151152 if target.startswith("http://") or target.startswith("https://"):153 resolved = target154 else:155 resolved = str(_resolve_path(target))156157 if system == "darwin":158 subprocess.run(["open", resolved], check=True)159 elif system == "windows":160 os.startfile(resolved) # type: ignore[attr-defined]161 else:162 subprocess.run(["xdg-open", resolved], check=True)163164 return {165 "opened": True,166 "target": resolved,167 }168169if __name__ == "__main__":170 app.run(transport="stdio")
这是一个紧凑的服务器,但重要的不是它的长度。重要的是接口的设计:
- 每个工具都有非常明确的职责
- 每个工具都返回结构化数据
- 命令执行受限制
- 文件访问限定在根目录
- 截图保存到已知文件夹
这正是你想要的本地 MCP 服务器。
为什么这些工具要这样设计
关于 Agent 工具的高质量内容绝不应止步于“这是代码”。工具的设计才是真正的经验。
list_files
这个工具为 Claude 提供了一个安全的探索面。它应该能回答这样的问题:
- 这个项目文件夹里有什么?
- Documents 里有哪些笔记?
- 是否已经有我可以检查的日志文件?
但它不应该成为递归的整盘爬虫。
read_file
这通常是最有用的本地工具之一。大量的实际工作仍然隐藏在本地 Markdown 笔记、日志、CSV 文件、文档和项目文件中。
max_chars 上限很重要。大文件既是上下文问题也是延迟问题。返回一个巨大日志文件的全部内容很少有用。
run_command
这是大多数人最容易疏忽的地方。
安全模式不是“允许 Shell 访问,然后听天由命”。安全模式是“允许一小组精确的、可审查的命令”。这就是示例中使用严格白名单的原因。
take_screenshot
截图工具很有价值,因为它让 Claude 能参与桌面工作流。即使你的第一个版本只将图片保存到磁盘,对于报告、UI 调试、文档捕获和结构化交接来说已经很有用了。
open_target
应用控制不需要从 GUI 自动化开始。对于许多工作流来说,“打开正确的文件夹、文件或 URL”就足够了。
这比在第一天就假装你需要完整的鼠标光标自动化要持久得多。
将服务器连接到 Claude
本地 MCP 服务器通常通过 stdio 运行,这意味着 Claude 在本地启动进程并通过 stdin/stdout 直接与之通信。对于本地计算机控制服务器,这是正确的默认设置,因为它避免了不必要的网络暴露 4 5。
Claude Desktop 通过配置支持本地 MCP 服务器,它会为你启动服务器进程。在实践中,为解释器和脚本使用绝对路径是最不脆弱的设置,因为本地 GUI 应用环境通常比你的终端更严格 2。
一个最小的配置如下:
1{2 "mcpServers": {3 "local-computer-control": {4 "command": "/absolute/path/to/python",5 "args": [6 "/absolute/path/to/local-mcp-server/server.py"7 ]8 }9 }10}
如果你更喜欢 uv,那也没问题:
1{2 "mcpServers": {3 "local-computer-control": {4 "command": "/absolute/path/to/uv",5 "args": [6 "--directory",7 "/absolute/path/to/local-mcp-server",8 "run",9 "python",10 "server.py"11 ]12 }13 }14}
保存配置并重启 Claude 后,服务器工具应该会出现在本地 MCP 工具列表中。Claude Desktop 的本地 MCP 设置正是基于这个模型:启动一个本地进程,通过 stdio 连接,并将工具暴露给模型 2 3。

实际有用的测试提示词
服务器连接后,不要一开始就尝试复杂的编排。从一个直接、枯燥的检查开始。
试试这些提示词:
- “列出我桌面文件夹里的文件。”
- “读取 ~/Documents/todo.md 并总结最重要的三件事。”
- “在我的本地项目文件夹中运行 git status 并解释有什么变化。”
- “拍一张名为 workspace-check 的截图。”
- “打开我的项目 README。”
如果这些简单的流程能稳定工作,那你就有了一个值得扩展的服务器。
如果不行,添加更多工具只会掩盖真正的问题。
本地 MCP 服务器真正有价值的地方
显而易见的用例是开发,但这只是其中一条路子。
开发者工作流
Claude 可以:
- 检查 repo 文件夹
- 读取配置文件
- 运行 git status
- 捕获错误状态的截图
- 打开项目目录
这已经消除了很多上下文切换。
研究工作流
Claude 可以:
- 列出研究文件夹
- 打开并总结 Markdown 笔记
- 读取结构化 CSV 文件
- 保存来自工具或仪表盘的截图
- 打开源文件或浏览器链接
内容工作流
Claude 可以:
- 扫描草稿文件夹
- 读取现有的帖子想法
- 捕获设计参考截图
- 打开正确的写作文件或 URL
- 运行一个有限命令来生成构建产物或草稿导出
运维工作流
Claude 可以:
- 检查已批准目录中的日志
- 运行只读的诊断命令
- 打开相关文件夹或仪表盘链接
- 保存证据截图
这才是这套架构的真正意义:不是作为噱头的“计算机控制”,而是工作流压缩。
安全层就是产品本身
这是太多技术文章轻描淡写的部分。
本地 MCP 的危险不在于协议,而在于糟糕的权限设计。
如果你想让这个服务器不止于演示,请尽早构建安全模型。

使用目录白名单
Claude 只能看到你明确批准的路径。这就是为什么 _resolve_path() 是文件工具的核心。
使用命令白名单
在第一版中永远不要暴露任意的 Shell 执行。从你可以逐行审核的精确命令开始。
将读取工具与操作工具分开
只读工具应该是默认的。操作工具应该有意地逐步引入。
记录所有操作
即使是一个简单的仅追加 JSON 日志,也能极大地提升调试和信任度。
为写入操作添加确认层
如果你以后添加 write_file、move_file 或 delete_file,请让这些工具要么需要第二个确认令牌,要么默认保持禁用。
考虑干运行模式
对于可执行操作的工具,干运行模式被低估了。它让 Claude 可以在执行前解释它将要做什么。
尽可能在受限用户下运行
如果你认真对待本地自动化,不要给你的 MCP 服务器超过所需的操作系统权限。
一个有用的经验法则:
- 级别 1: 只读工具
- 级别 2: 低风险操作,如打开文件/打开应用
- 级别 3: 需确认的写入操作
- 级别 4: 破坏性操作,通常不应随意暴露
大多数人永远不需要级别 4。

v1 之后可以改进的地方
一个坚实的第一版为你赢得变得更强大的权利。
一旦基本服务器稳定下来,下一个合理的升级包括:
- 集中化策略文件
将你的规则移到 config/policy.json 中,这样修改就是声明式的。
示例:
1{2 "allowed_roots": [3 "~/Documents",4 "~/Desktop",5 "./"6 ],7 "allowed_commands": [8 "pwd",9 "ls",10 "git status",11 "git diff --stat",12 "python --version"13 ]14}
- 结构化日志
将工具调用、时间戳、参数和结果记录到 logs/server.log 或 JSONL 文件中。
- 更安全的命令执行
不要使用一个通用的命令工具,而是将命令拆分成更窄的工具,比如:
- git_status
- show_current_directory
- list_project_files
这样 Claude 更容易选择工具,对你来说也更安全。
- 更好的截图处理
你可以从“将截图保存到磁盘”演变为:
- 带时间戳的截图
- 活动窗口截图
- 区域截图
- 文件保留规则
- 特定操作系统的自动化适配器
在 macOS 上,你以后可以添加 AppleScript 或 Shortcuts。在 Windows 上,添加 PowerShell 或 UI Automation。在 Linux 上,添加桌面特定的启动器和窗口工具。
但这应该在枯燥的本地核心功能可靠之后再进行。
人们在使用本地 MCP 服务器时常犯的错误
这些错误是可以预见的。
错误 1:过早赋予太多权力
人们喜欢完全控制电脑的想法,但讨厌调试它。从小处开始。
错误 2:模糊的工具名称
如果你的工具名称含糊不清,Claude 就会用得很糟糕。要明确。
不好的例子:
- system_action
- computer_control
更好的例子:
- list_files
- read_file
- run_command
- take_screenshot
- open_target
错误 3:非结构化输出
混合文本的块状内容比干净的 JSON 对象更难让 Claude 推理。
错误 4:没有日志
如果工具失败而你无法看到原因,系统就成了猜谜游戏。
错误 5:把模型当作控制层
Claude 是推理层。你的服务器仍然是执行层。
这个区别是不可妥协的。
这套架构比普通自动化好在哪
传统的桌面自动化通常是以下两种之一:
- 脆弱的 GUI 脚本
- 孤立的脚本,需要人类知道何时运行它们
本地 MCP 服务器改变了这一点,因为 Claude 可以根据用户的请求和可用上下文决定使用哪个工具。
这意味着你不仅仅是在自动化一个命令。你是在构建一个模型可以推理的本地能力层。
这就是为什么 MCP 感觉很重要。它不仅仅是另一种集成模式。它是一种更清晰的方式,将工具使用暴露给模型,而无需在应用层硬编码每一种可能的工作流。
你应该尊重的限制
即使是一个好的本地 MCP 服务器也有实际的限制。
- 桌面自动化在不同操作系统之间可能不稳定。
- 截图有用,但不是魔法。
- 启动应用容易;可靠的 UI 操作更难。
- 通用的 Shell 访问是危险的。
- 如果工具输出太大,上下文膨胀是真实存在的问题。
- 对于任何重要的事情,人类批准仍然有价值。
换句话说:不要混淆“模型可以采取行动”和“模型应该在没有监督的情况下行动”。
更有价值的模式是协作控制,而不是盲目自主。
![成田悠辅的天才 AI 使用技巧 [珍藏版]](https://youmind.club/__ym/cms-assets/media/1784137658627_u4bwry_HNMS89bbsAAUPJI.jpg)




