Skip to main content
Glama
OL486

notes-mcp

by OL486
README.md
# notes-mcp

把本地 Markdown 笔记目录以**只读**方式暴露给 AI 助手的 MCP Server。
基于 MCP 规范 `2026-07-28`,Python SDK `mcp[cli] >= 2.0`。

> 不写文件、不删文件、不执行任何外部命令。所有返回值都有字符预算上限。

---

## 它提供什么

**Tools(模型调用)**

| 名称 | 参数 | 行为 |
|---|---|---|
| `search_notes` | `query: str`, `limit: int = 5` | 全文检索,返回命中行 ±2 行的**片段**(每篇最多 3 段),总预算 8000 字符 |
| `read_note` | `path: str` | 读取单篇全文,超过 20000 字符截断并标注 |
| `list_recent` | `days: int = 7` | 最近 N 天修改过的笔记(路径 + 时间 + 首行标题) |
| `notes_stats` | — | 笔记总数、总字数、标签分布(解析 `#tag`) |

**Resources(客户端/用户读取)**

| URI | 说明 |
|---|---|
| `notes://index` | 全部笔记清单(JSON:uri / path / title / mtime / units / tags) |
| `notes://note/{+path}` | 单篇笔记正文,例如 `notes://note/daily/2026-10-25.md` |

**Prompts(用户触发的模板)**

| 名称 | 参数 | 说明 |
|---|---|---|
| `weekly-review` | `topic: str` | 生成"总结本周关于 {topic} 的笔记"的指令,要求 3 条要点 + 出处 |

---

## 快速开始

```bash
# 方式一:uv(推荐,与文档命令一致)
uv sync
uv run notes-mcp --root ./sample_notes

# 方式二:pip 可编辑安装
python -m venv .venv
.venv/Scripts/python -m pip install -e .      # Windows
# .venv/bin/python -m pip install -e .        # macOS / Linux
.venv/Scripts/python -m notes_mcp --root ./sample_notes
```

`--root` 也可以省略,此时按 `NOTES_MCP_ROOT` 环境变量 → `./notes` 的顺序取值。

### 构建可分发的产物

```bash
uv build      # -> dist/notes_mcp-0.1.0-py3-none-any.whl 与 dist/notes_mcp-0.1.0.tar.gz
```

把 wheel 拷到目标机器(不需要源码、不需要 uv):

```bash
python -m venv .venv
.venv/Scripts/python -m pip install notes_mcp-0.1.0-py3-none-any.whl
.venv/Scripts/notes-mcp --root D:\Notes --version
```

装完就多了一个 `notes-mcp` 命令,客户端配置里直接指向它,不用再记 `python -m notes_mcp`。

CI(`.github/workflows/ci.yml`)在每次推送时都会走一遍"构建 → 装进干净 venv → 跑 `--version`",
所以"构建成功"和"装得上"这两件事不会脱节。

### 验收(Inspector CLI,强制 modern 协议)

```bash
npx @modelcontextprotocol/inspector@2.12.0 --cli \
  --protocol-era modern \
  uv run notes-mcp --root ./sample_notes \
  --method tools/list
```

看到四个工具名即通过。`--protocol-era modern` 不能省——Inspector 的 CLI 在
ad-hoc 目标下默认走 **legacy**(2025 握手),不加这个参数你测的是旧时代的行为。

### 本地完整自检

```bash
uv run python scripts/verify.py      # 走真实 stdio 客户端,跑全部工具/资源/提示
uv run pytest                        # 单元测试 + 端到端测试
```

---

## 三条工程约束

### 1. 路径安全

`read_note` 的 `path` 来自模型,是不可信输入。校验流程(`notes_mcp/safety.py`):

1. 拒绝空、NUL 字节;
2. 拒绝绝对路径(同时覆盖 POSIX `/x`、Windows `C:\x`、`\\server\share`);
3. 拒绝任何含 `..` 的路径;
4. `Path.resolve()` **展开符号链接之后**,用 `is_relative_to(root)` 判断是否越界;
5. 只允许 `.md` / `.markdown`。

第 4 步是唯一真正有效的一步。字符串里看不出问题的 `escape.md` 完全可能是个
指向 `C:\Windows\System32\drivers\etc\hosts` 的软链接——只有 resolve 之后再比较才拦得住。

### 2. 上下文预算

`search_notes` 绝不返回整篇。每个片段前后各 2 行、每篇最多 3 段、总预算 8000 字符。
超限时返回文本里会带 `[已截断:…]`,明确告诉模型"你还没看完"。

完整预算表见 [`docs/WALKTHROUGH.md`](docs/WALKTHROUGH.md) 的 S4 一节。

### 3. 结构化错误

工具失败时不抛裸异常,而是 `raise ToolError("路径越界:…。只允许笔记根目录内的相对路径")`。
客户端收到 `isError: true` + 可读文本,模型能据此改写参数重试。
抛未捕获异常的话,模型只会看到 `Error executing tool read_note`,学不到任何东西。

---

## 接入客户端

### Claude Desktop

编辑 `%APPDATA%\Claude\claude_desktop_config.json`(macOS:`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "notes": {
      "command": "C:\\Users\\<你>\\projects\\notes-mcp\\.venv\\Scripts\\python.exe",
      "args": ["-m", "notes_mcp", "--root", "D:\\Notes"]
    }
  }
}
```

三个必须注意的点:

- Windows 路径在 JSON 里要用**双反斜杠**,否则 `\U`、`\n` 会被当转义序列;
- 用 `.venv` 里 python 的**绝对路径**,不要写 `uv run`——Claude Desktop 不继承你的 shell 环境,
  找不到 `uv` 就静默失败;
- 改完必须**完全退出** Claude Desktop(托盘图标也要退),它只在启动时读配置。

### Cursor

`~/.cursor/mcp.json`(项目级用 `.cursor/mcp.json`),结构同上:

```json
{
  "mcpServers": {
    "notes": {
      "command": "C:\\Users\\<你>\\projects\\notes-mcp\\.venv\\Scripts\\python.exe",
      "args": ["-m", "notes_mcp", "--root", "D:\\Notes"]
    }
  }
}
```

在 Cursor 的 Settings → MCP 里确认 `notes` 显示为已连接、工具数为 4。

### 用 wheel 装好的命令

如果是 `pip install notes_mcp-*.whl` 装的,直接指向虚拟环境里的可执行文件更省事:

```json
{
  "mcpServers": {
    "notes": {
      "command": "C:\\Users\\<你>\\notes-venv\\Scripts\\notes-mcp.exe",
      "args": ["--root", "D:\\Notes"]
    }
  }
}
```

macOS / Linux 上 `command` 换成 `/Users/<你>/notes-venv/bin/notes-mcp`。

---

## 配置项

| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `NOTES_MCP_ROOT` | `./notes` | 笔记根目录 |
| `NOTES_MCP_MAX_NOTE_CHARS` | `20000` | `read_note` / 单篇 Resource 的返回上限 |
| `NOTES_MCP_SEARCH_BUDGET` | `8000` | `search_notes` 总预算 |

命令行参数:`--root`、`--transport {stdio,streamable-http}`、`--host`、`--port`、
`--stateless-http`、`--log-level`。

完整的实施过程(每个阶段的协议报文、验收输出、踩过的坑)见
[`docs/WALKTHROUGH.md`](docs/WALKTHROUGH.md)。

---

## 项目结构

```
notes_mcp/
├── config.py     运行期配置(根目录 + 各类预算)
├── safety.py     不可信路径校验(工具与资源共用同一套规则)
├── store.py      扫描 / 解析 / 片段化检索(朴素实现)
├── budget.py     截断与预算拼装
├── tools.py      四个 Tool 的纯逻辑(不认识 MCP,可单测)
├── resources.py  两个 Resource 的纯逻辑
├── prompts.py    Prompt 模板
├── server.py     注册到 MCPServer
└── __main__.py   进程入口:参数、stderr 日志、传输
```

分层的关键是 `tools.py` / `resources.py` 不 import 任何 MCP 的东西。
它们只接收 `Settings` 和字符串,抛 `PathRejected` / `QueryRejected`;
`server.py` 才把这些异常翻译成 `ToolError` / `ResourceNotFoundError`。
这样单测不需要起 server,也不用解析 JSON-RPC。

---

## 故障排查

| 现象 | 判断依据 | 处理 |
|---|---|---|
| 客户端显示 "Server disconnected" | stderr 里有 traceback | 直接跑 `python -m notes_mcp --root ...` 看真实报错 |
| 工具列表是空的 | Inspector 没加 `--protocol-era modern` | 加上参数重测 |
| 客户端读到一半乱码 | stdout 混入了非协议输出 | 检查是否有 `print()`;日志必须走 stderr |
| `read_note` 报"笔记不存在" | 路径相对的是根目录,不是当前目录 | 先读 `notes://index` 拿准确路径 |

---

## 已知限制

- **朴素检索**:每次调用都全量扫描并读盘。几千篇以内无感,10 万篇需要倒排索引
  (改造方向见 `docs/WALKTHROUGH.md` S1 后半段)。
- **只读**:没有任何写路径。需要写入请另开一个 server,并显式做权限与备份。
- **单进程单根目录**:一个进程只服务一个 `--root`。多个目录就配多个 server 条目。

---

## 开发

```bash
uv sync                                  # 含 dev 依赖(pytest / anyio)
uv run pytest                            # 60 个用例
uv run python scripts/verify.py          # 22 项端到端检查(真实 stdio + 报文纯净度)
uv run python scripts/budget_report.py   # 打印各类返回的实测体积,确认没越预算
uv run python scripts/demo_weekly_review.py   # 按 prompt 模板走一遍完整调用链
```

实施过程与逐阶段协议报文见 [`docs/WALKTHROUGH.md`](docs/WALKTHROUGH.md),
可复现的验收结论见 [`ISSUE.md`](ISSUE.md)。

## 许可证

[MIT](LICENSE)

TDQS

A3.9/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct role: search_notes returns snippets, read_note returns full content, list_recent lists recently modified notes, and notes_stats summarizes corpus metadata. The descriptions explicitly clarify boundaries, especially search versus read.

Naming Consistency4/5

Three tools follow a verb_object pattern (search_notes, read_note, list_recent), while notes_stats is noun_noun. The deviation is minor and still readable, but it breaks strict consistency.

Tool Count5/5

Four tools are well-scoped for a focused notes retrieval server. Each tool earns its place and covers a distinct query pattern without bloat.

Completeness3/5

The read-only surface covers search, full read, recent listing, and stats, but there are no create, update, append, or delete operations. For a notes server, this is a notable gap if agents are expected to manage notes rather than only inspect them.

Maintenance

ActivityMaintained
ResponsivenessNo issues