Skip to main content
Glama
README.md
# 旦挞 MCP · DanTa MCP

[English](./README.en.md) | **中文**

让 AI 自动检索**复旦树洞**和**旦克课程评价**的 MCP 服务器。校外可用。

树洞是复旦学生的匿名社区——生活、情感、就业、心理、转专业、二手交易,什么都聊。
这个工具让 AI 帮你把这些真实经验搜出来、读完、汇总,而不是自己一页页翻。

```
你:帮我查一下高等微积分Ⅰ这门课怎么样

AI:(自动调用 search_courses → get_course_reviews)
    找到「高等微积分Ⅰ」(MATH20021),相辉学堂,5 学分,3 条评价。
    教师严金海 + 王嬴(习题课)
    综合评分 ★★★★★ / ★★★★☆
    - 给分:两位评价者最终都拿了 A,但都提到"课下要花很多时间"
    - 严老师:PPT 有用、往年卷会分享、题型固定不偏;但上课爱扯题外话
    - 王嬴老师:口碑极好,答疑响应快,改作业细致
    - 难度:期中偏难,秋季学期后考试强度有下降
```

---

## 目录

- [能干什么](#能干什么)
- [快速开始](#快速开始)
- [工具说明](#工具说明)
- [工作原理](#工作原理)
- [安全设计](#安全设计)
- [排障](#排障)
- [已知限制](#已知限制)
- [许可证](#许可证)

---

## 能干什么

配好之后直接用自然语言问,AI 会自动调工具:

| 场景 | 直接说 |
|---|---|
| 🍜 生活 | 「哪个食堂好吃」「宿舍空调怎么修」「校医院靠谱吗」 |
| 💔 情感 | 「树洞里异地恋都怎么处理的」「大家怎么看待校园恋爱」 |
| 💼 就业 | 「最近有什么实习内推」「信院就业前景怎么样」 |
| 🧠 心理 | 「大家怎么应对开学焦虑」「学校心理咨询好约吗」 |
| 🔄 学业 | 「转专业难不难,过来人怎么说」「保研经验」 |
| 👥 交友 | 「怎么找搭子」「社团值得进吗」 |
| 💰 交易 | 「二手电动车行情」「合租信息」 |
| 📚 选课 | 「高等微积分Ⅰ值得选吗」「对比这三门通识课」 |

数据来源:**复旦树洞**(实时匿名讨论,23000+ 话题标签)+ **旦克课程评价库**(结构化评分 + 学生长评)。

---

## 快速开始

### 前置要求

- Python 3.10+
- 一个复旦 UIS 账号(学号 + 密码)
- 一个旦挞/树洞账号(邮箱 + 密码,在 [旦挞 App](https://danxi.fduhole.com) 里注册)
- 一个支持 MCP 的客户端(Hermes / Claude Desktop / Cline 等)

### 1. 安装

```bash
git clone <你的仓库地址> danta-mcp
cd danta-mcp

# Windows
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt

# macOS / Linux
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```

### 2. 配置凭据

```bash
# Windows
.venv\Scripts\python setup_credentials.py

# macOS / Linux
.venv/bin/python setup_credentials.py
```

会依次问你两组账号(密码输入时不回显):

```
— 复旦 UIS 统一身份认证(学号 + 密码)
  用户名: 20307130001
  密码(不回显):
  ✅ 已保存

— 旦挞/树洞账号(邮箱 + 密码)
  用户名: 20307130001@m.fudan.edu.cn
  密码(不回显):
  ✅ 已保存
```

存储位置:
- **Windows** → 凭据管理器(DPAPI 加密,绑定你的 Windows 账户)
- **macOS/Linux** → 系统钥匙串(需 `pip install keyring`)
- **任何平台** → 也可用环境变量 `DANTA_UIS_USER` / `DANTA_UIS_PASS` / `DANTA_HOLE_USER` / `DANTA_HOLE_PASS`

验证:

```bash
.venv/Scripts/python setup_credentials.py --check
```

### 3. 自检

```bash
.venv/Scripts/python -E verify_mcp.py
```

应看到:

```
✅ handshake OK — 10 tools registered:
   • search_courses
   ...
✅ 连接正常
  WebVPN: 已建立会话
  用户 ID: 51359
✅ all checks passed
```

### 4. 接入 MCP 客户端

在客户端配置里加(**注意 `-E` 参数不能少**,见[排障](#排障)):

```json
{
  "mcpServers": {
    "danta": {
      "command": "/绝对路径/danta-mcp/.venv/Scripts/python.exe",
      "args": ["-E", "/绝对路径/danta-mcp/run_server.py"],
      "cwd": "/绝对路径/danta-mcp"
    }
  }
}
```

Hermes 用户写在 `config.yaml` 的 `mcp_servers:` 下(YAML 格式,字段同上)。

重启客户端即可。

---

## 工具说明

### 树洞检索(通用,什么话题都能搜)

#### `search_holes(keyword, limit=15, accurate=False, within_days=0)`
全站搜索树洞内容。**这是最常用的工具。**

```
search_holes("食堂")                      # 模糊搜索
search_holes("转专业", accurate=True)      # 精确匹配,关键词必须完整出现
search_holes("实习", within_days=14)       # 只看最近两周
```

- `accurate=True` — 精确匹配,适合专有名词、课程代码、人名
- `within_days=N` — 只看最近 N 天,适合找时效性内容(实习招聘、最新政策)

#### `get_hole(hole_id, limit=40)`
读某个树洞的全部楼层。

**树洞的价值常在回复里** —— 楼主提问,楼里的人给经验。搜到感兴趣的洞一定要读全文。

#### `browse_by_tag(tag, limit=15)`
按话题标签浏览最新树洞。比关键词搜索更适合"逛"某个话题。

常用标签:`提问` `求助` `生活` `学习` `恋爱` `情感` `吐槽` `emo` `交友`
`选课` `转专业` `保研` `期末考试` `出分` `二手交易` `找搭子` `军训` `家教`

#### `list_hot_tags(limit=40, keyword="")`
列出最热门的话题标签(按热度排序),可过滤。

```
list_hot_tags()                  # 看树洞上大家都在聊什么
list_hot_tags(keyword="实习")     # 找所有和实习相关的标签
```

树洞共有 23000+ 标签,热度前几名:提问(18万) 求助氵(9.5万) 生活(7.4万)
学习(6万) 恋爱(3.7万) 吐槽(2.3万) emo(1.8万)。

#### `browse_division(division_id, limit=15)` / `list_divisions()`
按板块浏览:

| ID | 板块 | 说明 |
|---|---|---|
| **1** | **茶楼** | **主板,什么都聊——日常首选** |
| 2 | 圆桌 | 深入讨论 |
| 3 | 评教 | 课程/教师评价 |
| 4 | 站务 | 论坛管理 |
| 5 | 交易 | 二手/合租/代购 |

### 课程评价(旦克)

#### `search_courses(keyword, limit=10)`
搜课程。keyword 可以是课程名、课程代码、关键词。

```
search_courses("微积分")
→ [1136] 微积分(上)(MATH120012) 基础医学院 | 评价数: 0
  [8911] 高等微积分Ⅰ (MATH20021) 相辉学堂 | 评价数: 3
```

**先看评价数**,为 0 的课没有参考价值。

#### `get_course_reviews(course_group_id, max_reviews=12)`
看某门课的完整评价,按「教师 + 学期」分组。

返回内容包含:
- 四维评分:**综合 / 内容 / 工作量 / 考核**(1-5 星)
- `remark` 推荐指数、点赞数
- 评价正文(通常含给分情况、考试难度、作业量、助教情况)

### 树洞

#### `search_holes(keyword, limit=15)`
全站搜索树洞内容。适合找老师风评、选课经验、真实吐槽。

#### `get_hole(hole_id, limit=40)`
读某个树洞的全部楼层。

#### `list_divisions()` / `browse_division(division_id, limit=15)`
板块浏览。板块 ID:

| ID | 板块 | 说明 |
|---|---|---|
| 1 | 茶楼 | 主论坛 |
| 2 | 圆桌 | 畅所欲言 |
| **3** | **评教** | **选课季重点看这个** |
| 4 | 站务 | 论坛管理 |
| 5 | 交易 | 非商业广告 |

### 排障

#### `build_citation_report(title, queries, summary_markdown="", per_query=6)`
把检索结果生成一份**带完整出处的 HTML 引证报告**,保存到桌面,浏览器打开即可。

每条引用保留:洞号(可点击溯源)、楼层、匿名昵称、时间、点赞数、**原文照登**。
适合需要人工核对结论的场景 —— 你可以逐条检查 AI 有没有过度解读。

```
build_citation_report(
  title="复旦留学生宿舍 · 快递地址怎么填",
  queries="北区驿站::北区 菜鸟驿站|顺丰京东::顺丰 京东 本部|hole:692300::地址怎么写",
  summary_markdown="<p>结论写在这里</p>"
)
```

`queries` 用 `|` 分隔多个来源分组,每组格式 `小节标题::关键词`;
也支持 `hole:洞号` 直接引用整个树洞的所有楼层。

报告特性:响应式布局、自动适配深色模式、内容全部 HTML 转义(防注入)。

#### `check_connection()`
检查 WebVPN 会话、token、账号状态。出问题时先跑这个。

---

## 工作原理

### 为什么需要这一层

复旦树洞的 API 全部部署在校内:

```
forum.fduhole.com → 10.107.13.152   ← 校内私有地址
auth.fduhole.com  → 10.107.13.152
danke.fduhole.com → 10.107.13.152
```

校外**物理上无法直连**。本项目复刻了[旦挞官方客户端](https://github.com/DanXi-Dev/DanXi)的 WebVPN 方案。

### 完整链路

```
1. 读取凭据(系统钥匙串 / 环境变量)
        ↓
2. UIS 登录 id.fudan.edu.cn
   getJsPublicKey → RSA-PKCS1 加密密码 → authExecute → loginToken
        ↓
3. authnEngine → CAS ticket (ST-xxxxx)
        ↓
4. 兑换 ticket → 拿到 WebVPN 会话 cookie
        ↓
5. API 域名 AES-CFB 加密改写
   https://auth.fduhole.com/api/login
   → https://webvpn.fudan.edu.cn/https/7772647670...c38/api/login
        ↓
6. 树洞账号换 JWT → 之后所有请求带 Bearer
```

第 5 步的加密:AES-CFB,key = iv = `wrdvpnisthebest!`,主机名按**字符数**(非字节数)补齐到 16 的倍数,输出 `iv_hex + 密文hex[:2n]`。

> 自检小技巧:`auth.fduhole.com` 加密结果恒为
> `77726476706e69737468656265737421f1e2559469366c45760785a9d6562c38`,
> 前 32 位正是 `wrdvpnisthebest!` 的 hex。对不上说明实现有问题。

### 缓存

会话和 token 缓存在 `~/.danta-mcp/`:
- `cookies.json` — WebVPN 会话,6 小时后视为过期
- `token.json` — 树洞 JWT,20 天

正常使用**不会反复登录**。

### 校内直连

报到后连校园网 / eduroam,直连即可通,WebVPN 这层自动变成透明兜底,代码不用改。

---

## 安全设计

### 密码不落盘明文

存在系统级凭据库,代码只读不写副本。仓库里也没有任何凭据(见 `.gitignore`)。

### UIS 登录单次尝试,失败即停

**这是最重要的一条设计。**

复旦 UIS 对连续登录失败会触发验证码,一旦触发,必须手动打开浏览器登录一次才能解除。所以:

```python
# authExecute 明确 tries=1,绝不重试
r = self._req("POST", ".../authExecute", tries=1, json={...})
```

检测到 `message` 含「验证码」时**立即抛错并提示手动登录**,不会自动重试轰炸。

网络层重试(`tries=3`)只作用于 WebVPN 的**不稳定连接**,不作用于登录尝试。

### 缓存文件权限

`~/.danta-mcp/` 下的文件权限设为 `600`(仅所有者可读写)。

### 只读

本项目**只提供读取工具**,不提供发帖、回复、投票等写操作。AI 不会替你在树洞发言。

---

## 排障

### `No module named 'mcp.server.fastmcp'`

MCP SDK 2.x 把 `FastMCP` 改名为 `MCPServer`。`server.py` 已做双版本兼容,如果仍报错,说明 venv 被外部 `PYTHONPATH` 污染了 → 见下条。

### 工具在客户端里不出现 / 服务器启动即退出

**九成是 `PYTHONPATH` 污染。** 某些 MCP 宿主会把自己的 `site-packages` 注入子进程环境,盖掉本项目 venv 里的包。

解法:配置里必须用 `-E`(启动时忽略环境变量):

```json
"args": ["-E", "/path/to/run_server.py"]
```

⚠️ 在代码里改 `sys.path` **是无效的** —— `sys.path` 在解释器启动时就由 `PYTHONPATH` 定型了。

### `❌ UIS 触发了验证码`

用浏览器打开 https://id.fudan.edu.cn 手动登录一次即可解除。

**不要反复重试**,会加重锁定。

### `login returned non-JSON`

WebVPN 会话过期,被弹回登录页。代码会自动重建会话重试一次。如果仍失败:

```bash
rm -rf ~/.danta-mcp   # 清缓存重新登录
```

### `IDP did not return a login context`

WebVPN 不稳(它的根路径 `/` 经常超时)。稍等重试即可。

### 改了 UIS 密码之后

```bash
.venv/Scripts/python setup_credentials.py
```

重新录入即可。

---

## 已知限制

- **依赖复旦认证系统的当前实现**。学校升级认证流程后可能失效,需跟进 [DanXi 上游](https://github.com/DanXi-Dev/DanXi) 的改动。
- **不支持双因素认证(2FA)**。如果你的账号开了增强认证,会抛 `EnhancedAuthenticationRequiredException`。
- **WebVPN 本身不稳定**,超时是常态,代码已加重试但无法完全消除。
- **课评数据取决于有没有人写**。冷门课可能一条评价都没有。
- 仅在 Windows 上完整测试过;macOS/Linux 的钥匙串路径写了但未实测。

---

## 联系

问题、建议或 bug 反馈:likangxian1007@gmail.com
或在 [Issues](https://github.com/likangxian1007/danta-mcp/issues) 提出。

---

## 许可证

**GPL-3.0** — 详见 [LICENSE](./LICENSE)。

本项目的 WebVPN 加密方案与 UIS 登录流程衍生自 [DanXi-Dev/DanXi](https://github.com/DanXi-Dev/DanXi)(GPL-3.0)。依据 GPL 传染性条款,本项目必须以相同许可证发布。

---

## 免责声明

本工具仅用于访问你**本人有权访问**的校内资源,等价于你手动登录网页所能看到的内容。

请遵守复旦大学网络使用规定和树洞社区公约。使用者对自己的行为负责。