内容
# 本地MCP服务器
一个用于安全访问本地文件系统的裸金属MCP服务器 — 单个客户端使用stdio,多客户端使用HTTP/SSE。只需一个标志即可在它们之间切换。零依赖。
```
AI 客户端 ── stdin/stdout ──▶ server.py ──▶ 您的文件
或 HTTP/SSE
```
> 无需 `pip install`。无需API密钥。无需云服务。仅需Python 3.8+和一个文件。
[](https://www.python.org/)
[](LICENSE)
[]()
[]()
在Towards Data Science上阅读完整的文章 → [我的AI对本地机器视而不见 — 我构建了缺失的MCP层](https://towardsdatascience.com/author/emmimalp-alexander/)
---
## 问题
每个连接AI到本地文件的MCP教程:
- 需要FastAPI、uvicorn、LangChain或官方MCP SDK
- 只实现规范定义的两种传输方式之一
- 在Windows上挂起,因为没有人在那里测试它
- 当指向一个大目录时,程序就会崩溃
MCP规范定义了 exactly 两种传输方式:stdio和HTTP/SSE。该服务器从头开始实现了这两种方式,只使用Python的标准库。
---
## 功能
```
AI 客户端 ──▶ 传输层 ──▶ 分发器 ──▶ 工具层 ──▶ 安全层 ──▶ 文件系统
(stdio / HTTP/SSE) (无状态) (4种工具) (MCP_ROOT 沙盒)
```
| 组件 | 任务 |
|---|---|
| StdioTransport | 单个客户端的基于行的JSON-RPC stdin/stdout。 |
| HTTPSSETransport | 请求的HTTP POST,响应的SSE流。16个工作线程。并发客户端。 |
| MCPDispatcher | 无状态的JSON-RPC路由器。无I/O。两种传输方式共享。 |
| 工具 | `list_directory`,`read_file`,`search_files`,`get_file_info` |
| 安全 | 每个路径在I/O之前都会相对于`MCP_ROOT`进行解析和验证。 |
---
## 快速入门
### 1. 克隆
```bash
git clone https://github.com/Emmimal/local-mcp-server.git
cd local-mcp-server
```
无需 `pip install`。无需 `requirements.txt`。
### 2. 运行演示
```bash
# stdio 传输
python demo.py C:/your/project/folder
# HTTP/SSE 传输
python demo.py C:/your/project/folder --http
```
预期输出:
```
============================================================
local-mcp-server demo [stdio transport]
根目录: C:/your/project/folder
============================================================
[1] 初始化
服务器 : local-mcp-server v1.0.0
协议 : 2024-11-05
[2] 可用工具
[list_directory ] 列出文件和目录...
[read_file ] 读取文件内容。最大1MB...
[search_files ] 按全局模式搜索文件...
[get_file_info ] 获取文件或目录的元数据...
[3] list_directory 8 entries
[4] get_file_info readable: True writable: True
[5] read_file 读取第一个小文件成功
[6] search_files 找到5个.py文件
============================================================
所有检查通过。准备连接本地桌面。
============================================================
```
### 3. 运行并发演示
```bash
# 终端1 — 启动HTTP/SSE服务器
python server.py --http --port 8765
# 终端2 — 启动5个同时运行的客户端
python concurrent_demo.py
```
```
============================================================
并发客户端演示 — 5个客户端,5个同时调用
============================================================
客户端 工具 结果 时间
---------- -------------------- ---------- --------
1 list_directory OK ~0.034s
2 get_file_info OK ~0.021s
3 list_directory OK ~0.038s
4 search_files OK ~0.023s
5 search_files OK ~0.021s
总壁时间: ~0.04s for 5个并发客户端
结果: ALL PASSED
============================================================
```
### 4. 连接到本地AI客户端
**macOS:** `~/Library/Application Support/Claude/local_desktop_config.json`
**Windows:** `%APPDATA%\Claude\local_desktop_config.json`
```json
{
"mcpServers": {
"local-desktop": {
"command": "python",
"args": ["/absolute/path/to/local-mcp-server/server.py"],
"env": {
"MCP_ROOT": "/absolute/path/to/your/workspace"
}
}
}
}
```
重启您的AI客户端。您将在工具菜单中看到`local-desktop`。
---
## 传输
### stdio — 单个客户端
```bash
python server.py
# 或使用显式根目录
python server.py --root C:/your/project
```
单个客户端。基于行的JSON-RPC stdin/stdout。桌面AI工具的MCP标准传输。
Windows注意:自动将二进制模式应用于stdin/stdout。否则,Python的默认文本模式会将`\n`转换为`\r\n`,从而破坏JSON。
### HTTP/SSE — 并发客户端
```bash
python server.py --http --port 8765
```
```
GET /sse 打开一个持久的SSE流 → 接收client_id
POST /message 发送JSON-RPC(包括?client_id=<uuid>)
GET /health 服务器状态和连接的客户端数量
GET / 信息页面
```
每个客户端都有自己的SSE流和消息队列。POST处理程序返回202立即 - 它不等待SSE交付。这种解耦使得并发工作。
---
## 四种工具
### list_directory
```json
{
"name": "list_directory",
"arguments": { "path": "src", "show_hidden": false }
}
```
返回每个条目的名称、类型、大小、修改时间戳和相对路径。目录在文件之前排序。默认情况下,隐藏条目被排除。
### read_file
```json
{
"name": "read_file",
"arguments": { "path": "src/main.py" }
}
```
以UTF-8文本形式返回文件内容。二进制文件以 `{ "binary": true, "data": "<base64>" }` 返回。硬上限:1MB。如果需要,可以在`server.py`中更改`MAX_FILE_BYTES`。
### search_files
```json
{
"name": "search_files",
"arguments": { "pattern": "*.py", "recursive": false, "max_results": 20 }
}
```
**默认情况下为浅层搜索。** 传递`recursive: true`进行深度搜索。第一个版本默认使用`rglob()` - 将其指向`C:\Users\Admin`运行了十分钟。现在默认`recursive=False`,服务器本身无法更改。
响应中的`truncated`标志告诉客户端结果何时在`max_results`处被截断。
### get_file_info
```json
{
"name": "get_file_info",
"arguments": { "path": "README.md" }
}
```
返回名称、类型、大小、扩展名、修改时间、创建时间、可读性和可写性。使用`os.access()`进行实际权限检查,而不仅仅是存在检查。
---
## 安全模型
`MCP_ROOT` 是服务器可以访问的**唯一**目录。
```python
def is_safe_path(base: Path, target: Path) -> bool:
try:
target.resolve().relative_to(base.resolve())
return True
except ValueError:
return False
```
每个路径:
1. 与`MCP_ROOT`连接
2. 完全解析 - 展开符号链接,折叠`..`
3. 使用`relative_to()`检查 - 如果超出根目录,则引发`ValueError`
| 攻击 | 结果 |
|---|---|
| `../../etc/passwd` | 访问被拒绝 |
| 指向根目录外的符号链接 | 访问被拒绝 |
| Windows UNC路径 `\\server\share` | 访问被拒绝 |
| `src/main.py` 在根目录内 | 允许 |
**将`MCP_ROOT`设置为工作流实际需要的最小目录。**
---
## 命令行选项
```
python server.py [选项]
--http 使用HTTP/SSE传输(默认:stdio)
--host HOST HTTP绑定主机(默认:127.0.0.1)
--port PORT HTTP端口(默认:8765)
--root PATH 覆盖MCP_ROOT环境变量
--debug 启用详细调试日志记录到mcp_server.log
```
---
## 运行测试
```bash
pip install pytest
python -m pytest tests/test_server.py -v
# 跳过HTTP集成测试
python -m pytest tests/test_server.py -v -m "not integration"
```
50个测试,涵盖七个类:
| 类 | 它涵盖的内容 |
|---|---|
| TestSecurity | 遍历攻击,符号链接逃逸,空路径 |
| TestListDirectory | 隐藏文件,排序顺序,锁定条目,错误 |
| TestReadFile | 文本,二进制/base64,1MB上限,权限错误 |
| TestSearchFiles | 浅层与递归,最大结果,截断标志 |
| TestGetFileInfo | 文件与目录,权限,时间戳 |
| TestDispatcher | 所有方法,通知,解析错误,未知方法 |
| TestHTTPTransport | 健康端点,SSE连接,400/404错误代码 |
---
## 项目结构
```
local-mcp-server/
├── server.py # 所有内容。两种传输,所有工具,安全。
├── demo.py # 在连接前验证stdio或HTTP/SSE
├── concurrent_demo.py # 证明5个客户端同时运行
├── http_client.py # 最小的标准库HTTP/SSE客户端示例
├── tests/
│ └── test_server.py # 50个测试
└── local_desktop_config.example.json # 配置模板
```
---
## 性能(Windows 11,Python 3.12.6,仅CPU)
| 操作 | 延迟 |
|---|---|
| list_directory(8个条目) | < 5ms |
| read_file(< 1MB文本) | < 5ms |
| search_files(浅层,5个结果) | < 30ms |
| get_file_info | < 5ms |
| 5个并发HTTP/SSE客户端 | ~40ms总壁时间 |
搜索是唯一具有显著延迟变化的工具。浅层搜索项目文件夹速度很快。`recursive=True`在大型目录树上将很慢 - 这是预期的,也是故意的。
---
## 开发过程中出现的问题
只有三个问题在服务器运行在真实机器上时出现:
**十分钟挂起。** `rglob()`在`C:\Users\Admin`上扫描了数万个文件。 通过使搜索默认浅层来修复。
**Windows `\r\n` 问题。** Python在Windows上的默认文本模式会将`\n`转换为`\r\n`,从而破坏JSON。 使用`msvcrt.setmode(O_BINARY)`修复了stdin和stdout上的问题。
**二进制文件崩溃。** 读取`.pyc`文件时出现`UnicodeDecodeError`。 使用base64回退修复。
**200MB数据库冻结。** 没有文件大小限制意味着一次意外的SQLite读取使进程冻结了30秒。 通过设置`MAX_FILE_BYTES = 1_048_576`修复。
---
## 已知限制
- **16个工作线程** 足够用于本地使用。 不适用于具有数百个同时连接的共享服务器 - 请使用`asyncio`。
- **1MB文件限制** 是一个常量。 如果工作流需要更大的文件,请在`server.py`中更改`MAX_FILE_BYTES`。
- **不包含令牌计数。** 返回原始文件内容。 令牌预算管理应在该服务器和模型之间的层中完成。
- **内存不持久。** 服务器没有会话状态。 每个连接从头开始。
---
## 相关
同一系列 - AI系统的生产层:
- [我的AI对本地机器视而不见 - 我构建了缺失的MCP层](https://towardsdatascience.com) - 本文。 裸金属MCP服务器,stdio + HTTP/SSE,零依赖。
- [RAG还不够 - 我构建了缺失的上下文层,使LLM系统工作](https://towardsdatascience.com) - 上下文管理层:检索,重新排序,内存衰减,令牌预算。 [GitHub →](https://github.com/Emmimal/context-engine)
- [RAG对时间视而不见 - 我构建了一个时间层来修复它](https://towardsdatascience.com/rag-is-blind-to-time-i-built-a-temporal-layer-to-fix-it-in-production/) - 时间感知层, 将文档新鲜度视为首要检索信号。
- [LLM评估基于感觉 - 我构建了缺失的层来决定什么发布](https://towardsdatascience.com/llm-evals-are-based-on-vibes-i-built-the-missing-layer-that-decides-what-ships/) - 评估层, 替换直觉式发布决策为可衡量输出质量门控。
- [提示工程还不够 - 我构建了一个适用于生产的控制层](https://towardsdatascience.com/prompt-engineering-isnt-enough-i-built-a-control-layer-that-works-in-production/) - 适用于生产LLM系统的提示控制层。
- [RAG正在烧钱 - 我构建了一个成本控制层来修复它](https://towardsdatascience.com/rag-is-burning-money-i-built-a-cost-control-layer-to-fix-it/) - 成本控制层, 在不降低输出质量的情况下削减RAG推理支出。
---
由 [Emmimal P Alexander](https://emitechlogic.com/) 构建 - 关于生产AI系统和使它们工作的层的文章。
---
## 许可证
MIT。 您可以随心所欲地使用它。
---
## 贡献
欢迎PR。 硬性规则: **没有新依赖。** 如果您的功能需要软件包,它属于一个分支。