Metadata-Version: 2.4
Name: code-editor-mcp
Version: 0.2.5
Summary: Code editor MCP server with sandboxing, optimistic locking, and path whitelists.
Project-URL: Homepage, https://github.com/TripQi/code-editor
Project-URL: Repository, https://github.com/TripQi/code-editor
Project-URL: Issues, https://github.com/TripQi/code-editor/issues
Author: TripQi
License-Expression: MIT
License-File: LICENSE
Keywords: mcp,文件编辑
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Requires-Dist: charset-normalizer>=3.3.2
Requires-Dist: mcp>=1.22.0
Requires-Dist: send2trash>=1.8.3
Description-Content-Type: text/markdown

## code-editor

面向多客户端并发、安全沙箱、编码感知的代码编辑 MCP 服务器。
> 路径访问更新：**所有参数必须是绝对路径**。除 `read_files` 外，其余文件/目录工具仍校验是否落在允许目录列表内。`CODE_EDIT_ROOT` 只是安全标记，不做访问边界或相对路径解析；访问新目录请用 `config_ops(action="set_root", path="...")` 加入允许列表。

### 安装与运行
```bash
# 安装（pip 或 uv 均可，包名 code-editor-mcp）
pip install code-editor-mcp         # 或 uv pip install code-editor-mcp

# 更新最新版
pip install -U code-editor-mcp

# 直接启动 CLI 入口
code-editor

# 若从源码运行
uv sync
uv run python server.py
```

关键环境变量：
- `CODE_EDIT_ROOT`：安全标记（默认启动时的 CWD），不做访问边界，也不参与相对路径解析。
- `CODE_EDIT_ALLOWED_ROOTS_FILE`：允许目录持久化 JSON，默认 `tools/.code_edit_roots.json`。
- `CODE_EDIT_ALLOWED_DIRECTORIES`：逗号分隔允许目录列表（兼容旧的 `CODE_EDIT_ALLOWED_ROOTS`）。

环境变量一览：

| 变量 | 作用 | 默认值 |
| --- | --- | --- |
| `CODE_EDIT_ROOT` | 安全标记（非访问边界，不做路径解析） | 当前工作目录 |
| `CODE_EDIT_ALLOWED_ROOTS_FILE` | 允许目录持久化文件 | `tools/.code_edit_roots.json` |
| `CODE_EDIT_ALLOWED_DIRECTORIES` (兼容 `CODE_EDIT_ALLOWED_ROOTS`) | 额外允许目录（逗号分隔） | 空 |
| `CODE_EDIT_FILE_READ_LINE_LIMIT` | （legacy）内部 `read_file` 默认行数上限；`read_files` v2 使用 `page_line_count/max_lines_per_snippet` 控制输出 | 1000 |
| `CODE_EDIT_FILE_WRITE_LINE_LIMIT` | `file_ops` 写入行数警戒 | 50 |

### 设计要点
- 允许目录列表：默认允许用户主目录；路径需落在允许目录内，否则拒绝。`config_ops(action="set_root")` 仅将目录加入允许列表并更新安全标记，不做路径拼接。
- 持久允许目录：`config_ops(action="set_root")` 成功后写入 JSON，可跨会话复用。
- 乐观锁：写/删类支持秒或纳秒级 `expected_mtime`，10ms 容忍；写入走原子写避免部分落盘。
- 默认忽略：`.git`、`__pycache__`、`node_modules`、`.DS_Store`、`.env*`、`.venv`、`*.log`、`*.pem`；`ignore_patterns` 传空字符串/空列表可关闭默认忽略。
- 编码感知：`read_files` v2 默认逐文件 `auto` 检测/复用编码（基于最新 mtime 刷新的元信息缓存）；如自动结果乱码，可在每个 request 里显式传 `encoding` 覆盖。
- 安全删除：禁止删除当前根/其祖先/关键系统目录。

### MCP 工具（code-editor）

#### 文件系统工具

| 名称 | 工具 | 功能 | 主要参数/说明 | 常见误用 |
| --- | --- | --- | --- | --- |
| `config_ops` | `config_ops(action, path=None)` | 允许目录管理 + 元信息 | `action`=set_root/list_roots/get_info；`path` 仅 set_root/get_info 需要 | 拼错 action；误以为 list_roots 需要 path |
| `read_files` | `read_files(requests, default_encoding="auto", page_line_count=400, max_snippets=64, max_total_chars=200000, max_chars_per_snippet=50000, max_lines_per_snippet=2000, parallelism=4)` | LLM 友好读取：多文件/同文件多片段 + 分页 | `requests` 为 `list[dict]`：支持 `mode`=`range`/`tail`/`all`；行号 **1-based**；可用 `cursor` 继续分页；返回结构化 `results[]`，每项含 `content`（纯内容）、`start_line/end_line`、`core_*`、`encoding`、`sha256_utf8`、可选 `next_cursor`；`encoding` 支持 `auto/utf-8/gbk/gb2312/gb18030`；**仅此工具允许读取任意绝对路径，不受允许目录白名单限制** | 继续传旧的 `file_paths`；把 `start_line` 当 0-based；一次请求太多片段触发 `max_total_chars`；对图片用 range/tail |
| `dir_ops` | `dir_ops(action, dir_path, depth=2, format="tree"|"flat", ignore_patterns=None, max_items=1000, expected_mtime=None, confirm_token=None, allow_nonempty=None, approval_token=None)` | 统一目录操作 | `action`=create/list/delete；绝对路径；list: tree 返回字符串列表、flat 返回字典列表；`ignore_patterns` 为 None 用默认忽略，空字符串/空列表关闭默认忽略；flat 下 `max_items` 限制返回条数；delete: 必须提供 `expected_mtime`、`confirm_token`、`allow_nonempty`，`confirm_token`=`delete:<normalized_abs_path>`（Path.resolve + os.path.normcase）；高风险删除会返回结构化 `needs_approval` 并要求 `approval_token` 二次提交 | action 不支持/参数缺失；delete 未显式 allow_nonempty；confirm_token 不匹配 |
| `file_ops` | `file_ops(action, file_path=None, content=None, source_path=None, destination_path=None, expected_mtime=None, encoding="utf-8", approval_token=None)` | 综合文件操作：write/append/copy/move/delete | 所有路径必须绝对且在允许目录内；write 覆盖、append 追加；write/append 需 file_path+content；copy/move 需 source_path+destination_path；delete 需 file_path；encoding 仅写入使用；expected_mtime：写/删校验目标文件，拷贝/移动校验源文件；删除返回结构化结果 | action 不支持或参数缺失；copy 目标已存在；delete 目标是目录 |
| `convert_file_encoding` | `convert_file_encoding(file_paths, source_encoding, target_encoding, error_handling="strict", mismatch_policy="warn-skip")` | 批量转码并覆盖写回 | 绝对路径列表；utf-8/gbk/gb2312/gb18030；错误处理 strict/replace/ignore；编码检测( charset-normalizer )，策略 fail-fast / warn-skip(默认) / force；结果返回 detectedEncoding/Confidence/mismatch；内置别名兼容 utf8/utf_8/utf-8-sig/ascii/cp936/gb-2312/gb-18030 | 相对路径；二进制文件；未在白名单 |

#### dir_ops 参数要点（避免误调用）
- `create`：仅 `dir_path` 必须；其余参数会被忽略。
- `list`：`format` 仅支持 `tree`/`flat`；`max_items` 必须为正整数或 None。
- `delete`：必须同时提供 `expected_mtime`、`confirm_token`、`allow_nonempty`（显式 True/False）。
- 删除行为统一移动到回收站，不支持永久删除。
- 命中高风险规则时会返回 `status=needs_approval`，响应内包含 `approval_token` 与 `llm_instruction`。
- 盘符根目录（如 `C:\` / `D:\` / `E:\`）会被永久阻断并返回 `status=blocked_critical`。
- `confirm_token` 生成规则（严格匹配）：
```python
from pathlib import Path
import os

normalized = os.path.normcase(str(Path(dir_path).resolve()))
confirm_token = f"delete:{normalized}"
```

#### 代码精准编辑工具

| 名称 | 工具 | 功能 | 主要参数/说明 | 常见误用 |
| --- | --- | --- | --- | --- |
| `edit_blocks` | `edit_blocks(edits, error_policy="fail-fast", encoding="auto")` | 单编辑/批量编辑：搜索替换 | `edits` 支持 `dict`（单编辑，支持大文件）或 `list[dict]`（批量，支持同文件多处与多文件）；每个 edit 必须有 `file_path`（注意不是 `path`）；支持 `encoding="auto"` 自动检测；批量模式支持 `error_policy`: fail-fast/continue/rollback | 把 `read_files` 的 `path` 当成 `file_path`；传了不支持的编码；批量模式传空列表；大文件用批量模式（应改为单编辑 dict） |

#### edit_blocks 返回格式（v0.3.0+）

`edit_blocks` 返回结构化数据，包含修改位置信息便于验证：

```python
# edit_blocks（单编辑）返回示例
{
    "status": "success",
    "message": "Applied 2 edit(s) to /path/file.py (lines 10-12, 20-25)",
    "file_path": "/path/file.py",
    "replacements": 2,
    "locations": [
        {"start_line": 10, "end_line": 12, "start_col": 5, "end_col": 20},
        {"start_line": 20, "end_line": 25, "start_col": 1, "end_col": 15}
    ]
}

# edit_blocks（批量）返回示例
{
    "status": "success",  # success/partial/error
    "message": "Completed 3/3 edits",
    "total_edits": 3,
    "successful_edits": 3,
    "failed_edits": 0,
    "results": [
        {"status": "success", "file_path": "...", "replacements": 1, "locations": [...], "message": "..."},
        {"status": "success", "file_path": "...", "replacements": 1, "locations": [...], "message": "..."},
        {"status": "success", "file_path": "...", "replacements": 1, "locations": [...], "message": "..."}
    ]
}
```

#### 使用示例

- 查看并新增允许目录：`config_ops(action="list_roots")` → 若未包含目标，调用 `config_ops(action="set_root", path="/data/project")`。
- 带锁写入：`info = config_ops(action="get_info", path="/abs/path/src/app.py")` → `file_ops(action="write", file_path="/abs/path/src/app.py", content=content, expected_mtime=info["modified"])`。
- 精确替换（单编辑）：`edit_blocks(edits={"file_path":"/abs/path/src/app.py","old_string":"old","new_string":"new","expected_replacements":1,"expected_mtime":info["modified"]})`。
- 单文件片段读取（range，行号 1-based）：
```python
read_files([
  {"path": "/abs/path/src/app.py", "mode": "range", "start_line": 10, "line_count": 60}
])
```
- 同文件多片段 + 跨文件（一次拿齐，减少多次调用）：
```python
read_files([
  {"path": "/abs/path/src/app.py", "mode": "range", "start_line": 1, "line_count": 80, "id": "app_head"},
  {"path": "/abs/path/src/app.py", "mode": "range", "start_line": 200, "line_count": 80, "id": "app_mid"},
  {"path": "/abs/path/README.md", "mode": "range", "start_line": 1, "line_count": 120, "id": "readme"},
], max_total_chars=200000, parallelism=4)
```
- 分页读取整文件（all + cursor）：
```python
resp1 = read_files([{"path": "/abs/path/src/app.py", "mode": "all"}], page_line_count=400)
cursor = resp1["results"][0].get("next_cursor")
resp2 = read_files([{"cursor": cursor}], page_line_count=400)  # 继续下一页
```
- 批量编辑（同文件多处修改）：
```python
edit_blocks(edits=[
    {"file_path": "/abs/app.py", "old_string": "foo", "new_string": "bar"},
    {"file_path": "/abs/app.py", "old_string": "hello", "new_string": "world"},
], error_policy="rollback")
```
- 批量编辑（多文件）：
```python
edit_blocks(edits=[
    {"file_path": "/abs/a.py", "old_string": "v1", "new_string": "v2"},
    {"file_path": "/abs/b.py", "old_string": "v1", "new_string": "v2"},
], error_policy="continue")
```
- 批量转码：`convert_file_encoding(["/abs/a.txt", "/abs/b.txt"], "gb2312", "utf-8", error_handling="replace", mismatch_policy="warn-skip")`。
- 列目录（扁平）：`dir_ops(action="list", dir_path="/abs/path", format="flat", ignore_patterns=[".git", "node_modules"])`。
- 删除目录（显式确认，统一进回收站）：`info = config_ops(action="get_info", path="/abs/path")` → `normalized = os.path.normcase(str(Path("/abs/path").resolve()))` → `token = f"delete:{normalized}"` → `dir_ops(action="delete", dir_path="/abs/path", expected_mtime=info["modified"], confirm_token=token, allow_nonempty=True)`。
- 高风险删除二次审批：首次调用 delete 可能返回 `status=needs_approval` + `approval.approval_token`，拿到用户明确同意后，带上 `approval_token` 再次调用同一 delete。

### MCP 客户端快速配置示例

```json
{
  "mcpServers": {
    "code-editor": {
      "command": "code-editor",
      "env": {
        "CODE_EDIT_ROOT": "."
      }
    }
  }
}
```
```toml
[mcp_servers.code-editor]
command = "code-editor"
# 将工作目录指向当前项目,使 CODE_EDIT_ROOT 默认跟随启动时的 CWD
cwd = "."
startup_timeout_sec = 120
```

### 安全/行为提示
- 路径验证：除 `read_files` 外，其他操作要求**绝对路径**且落在允许目录列表内；`read_files` 仅要求绝对路径；`CODE_EDIT_ROOT` 仅为安全标记。
- 删除防护：删除只允许移动到回收站；任何失败都会直接拒绝（不会降级为永久删除）。
- 盘符根目录阻断：`C:\`、`D:\`、`E:\`（及其他文件系统根目录）会被永久阻断，并返回极高风险警告。
- 二次审批：仅“重要目录 + 大规模删除”触发；返回会强制提示模型先询问用户并等待批准。
- 允许目录管理：如需访问新路径，先 `config_ops(action="set_root")` 加入白名单；不在白名单的绝对路径会被拒绝。
