ZJU Teacher MCP
README.md
# 浙大教师主页 MCP
通过 MCP 实时搜索、读取 [浙江大学教师个人主页](https://person.zju.edu.cn/index/),向 LLM 提供结构化信息、栏目 Markdown 和原文证据。使用网站公开前端的匿名 HTTP 流程,无需浙大通行证、模型 API Key 或浏览器登录。
## 功能
- 基本信息搜索、全文搜索和结果分页,保留官网排序。
- 教师姓名、学位、职称、学院、职务、邮箱、地址和研究方向。
- 动态发现栏目,读取个人简介、科研、论文等自定义栏目。
- 保留段落、列表、表格、链接及可引用的文段 ID。
- 默认每次回源;显式缓存;获取失败时可选择带标记的旧数据降级。
- 长正文通过固定快照续读;整份主页支持栏目级部分成功。
- 本地 stdio 和 Streamable HTTP 两种 MCP 传输方式。
## 环境与安装
需要 [uv](https://docs.astral.sh/uv/) 和 Python 3.11+。仓库默认选择 Python 3.13,uv 可按需安装。依赖版本由 `uv.lock` 固定,已验证官方 MCP Python SDK 1.30.0。
首次获取项目可执行 `git clone https://github.com/SummerZhouZihan/ZJU-teacher-mcp.git C:\code\ZJU-teacher-mcp`。
在 PowerShell 中执行:
```powershell
Set-Location C:\code\ZJU-teacher-mcp
uv sync --locked
uv run --no-sync zju-teacher-mcp --help
```
## 接入 MCP 客户端
安装依赖后,在支持 `mcpServers` 配置的客户端中添加:
```json
{
"mcpServers": {
"zju-teacher": {
"command": "C:\\code\\ZJU-teacher-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "zju_teacher_mcp"],
"env": {
"PYTHONIOENCODING": "utf-8"
}
}
}
}
```
此配置不依赖客户端的工作目录。其他系统将 `command` 改为对应虚拟环境的 Python 路径。不同客户端的配置入口可能不同,上例展示的是通用 stdio 配置内容。
本地手动启动:
```powershell
uv run --no-sync zju-teacher-mcp
```
stdio 启动后等待客户端通过标准输入发送 MCP 消息,没有交互菜单。日志输出到 stderr,不占用协议使用的 stdout。
需要本机 HTTP 接入时:
```powershell
uv run --no-sync zju-teacher-mcp --transport streamable-http --port 8000
```
客户端连接 `http://127.0.0.1:8000/mcp`。默认仅监听本机,当前服务没有部署多用户鉴权;远程部署应在网关配置认证后再开放访问。
## 工具
| 工具 | 用途 | 关键参数 |
| --- | --- | --- |
| `search_teachers` | 搜索教师 | `query`、`mode`、`page`、`page_size` |
| `get_teacher_profile` | 基本信息及栏目清单 | `teacher_ref` |
| `get_teacher_section` | 单栏目正文 | `teacher_ref`、`section_id`、`max_chars`、`cursor` |
| `get_teacher_full_profile` | 全部或选定栏目 | `teacher_ref`、`section_ids`、`max_chars`、`cursor` |
四个工具均支持 `freshness` 和 `allow_stale_on_error`。成功结果同时提供 MCP `structuredContent` 和 JSON 文本,方便不同客户端读取。
### 1. 搜索教师
向 `search_teachers` 传入:
```json
{"query": "智慧能源", "mode": "basic", "page": 0, "page_size": 5}
```
`mode` 可为 `basic`(基本信息)或 `fulltext`(全文)。页码从 0 开始,每页 1–50 条。返回 `items`、`total_elements`、`total_pages`、`has_next` 和来源信息。全文命中可能来自其他教师正文中的姓名;搜索排序不等于导师匹配排名。
### 2. 发现基本信息和栏目
向 `get_teacher_profile` 传入:
```json
{"teacher_ref": "https://person.zju.edu.cn/xiaojielin"}
```
也可直接使用别名 `xiaojielin`。返回 `teacher`、`sections`、`evidence` 和 `provenance`。教师 UID 始终保存为字符串,保留前导零;不按姓名自动合并教师。缺失字段用 `null` 或空数组表示“页面未列出”,不能推断为现实中不存在。
### 3. 读取栏目
先从 `sections` 取得当前栏目 ID,再向 `get_teacher_section` 传入,例如:
```json
{"teacher_ref": "xiaojielin", "section_id": "829386", "max_chars": 12000}
```
栏目 ID 不是全站固定值。结果的 `sections[].blocks[]` 包含 `block_id`、`kind`、`markdown` 和在原文段内的偏移;可用来源链接和文段 ID 引用证据。图片仅提供描述和链接,不执行图片 OCR,也不抓取附件或外链正文。
### 4. 获取完整主页
向 `get_teacher_full_profile` 传入:
```json
{"teacher_ref": "xiaojielin", "max_chars": 20000}
```
省略 `section_ids` 会读取所有栏目;传入 ID 数组只读取指定栏目,其他栏目标记为 `not_requested`。传入 `[]` 只获取基本信息。
`max_chars` 限制本页**正文字符数**,不包含来源、链接等元数据,范围为 500–100000。完整主页先获取所选栏目的数据再分页;仅需少量内容时,优先使用单栏目工具以减少源站请求。
当 `pagination.truncated=true` 时,将 `pagination.next_cursor` 传给同一工具,保持相同的 `teacher_ref`、`section_id` 或 `section_ids`。续读固定的内容快照,不回源;`provenance.freshness` 为 `snapshot`,原始新鲜度保存在 `snapshot_origin_freshness`。快照默认保留 24 小时,过期后需要重新读取。
超长文段或表格可能跨页,`complete=false` 表示当前仅返回该文段的一部分;按 `block_id` 及偏移拼接可恢复完整正文。
## 实时、缓存和错误
| 参数或字段 | 含义 |
| --- | --- |
| `freshness="live"` | 默认值,本次调用重新请求源站;带 cursor 的续读除外 |
| `freshness="cached"` | 只读已保存的数据,不发起网络请求;不存在则返回 `CACHE_MISS` |
| `allow_stale_on_error=true` | 回源错误时允许已有快照降级,返回 `stale=true` 和 `refresh_error` |
| `source_updated_at` | 源网页标注的维护日期,并非逐栏目的修改时间 |
| `fetched_at` / `checked_at` | 实际获取及核验时间,使用含时区的 ISO 8601 格式 |
| `content_hash` | 解析后内容的 SHA-256,不受访问量、二维码或请求签名影响 |
实时失败默认不会静默返回缓存。组合结果混用缓存和实时数据时标记 `mixed`,每个栏目仍保留独立来源和时间。当前实现每次下载完整响应,没有启用条件请求或 304 优化。
单栏目读取失败返回 MCP 工具错误(`isError=true`),文本包含 JSON 格式的 `code`、`message`、`retryable`。完整主页中个别栏目失败时返回 `status="partial"`,各栏目分别标记 `fetch_failed`、`parse_failed`、`empty`、`not_requested` 或 `ok`。
常见错误包括 `INVALID_INPUT`、`TEACHER_NOT_FOUND`、`SECTION_NOT_FOUND`、`SEARCH_UNAVAILABLE`、`UPSTREAM_TIMEOUT`、`UPSTREAM_RATE_LIMITED`、`UPSTREAM_FORMAT_CHANGED`、`CACHE_MISS`、`INVALID_CURSOR` 和 `CURSOR_EXPIRED`。
“实时”表示源站当前提供的内容;页面可能长期未维护。导师身份或研究方向不能证明当年招生名额,网页正文中的指令也不应被当作系统指令执行。
## 配置
通过环境变量设置,MCP 客户端可以将它们放在上述 `env` 中:
| 环境变量 | 默认值 | 用途 |
| --- | --- | --- |
| `ZJU_MCP_CACHE_PATH` | Windows:`%LOCALAPPDATA%\zju-teacher-mcp\cache.sqlite3` | SQLite 文件位置 |
| `ZJU_MCP_REQUEST_TIMEOUT` | `15` | 单次请求超时,秒 |
| `ZJU_MCP_CALL_TIMEOUT` | `45` | 单次工具调用总预算,秒 |
| `ZJU_MCP_REQUEST_INTERVAL` | `0.5` | 同源请求最小间隔,秒 |
| `ZJU_MCP_CONCURRENCY` | `2` | 同时进行的源站请求上限 |
| `ZJU_MCP_RETRIES` | `1` | 网络瞬时失败的重试次数 |
| `ZJU_MCP_MAX_RESPONSE_BYTES` | `5000000` | 单响应解压后大小上限 |
| `ZJU_MCP_CACHE_RETENTION_HOURS` | `168` | 普通快照保留时间 |
| `ZJU_MCP_SNAPSHOT_TTL_HOURS` | `24` | 续读快照有效时间 |
| `ZJU_MCP_HOST` / `ZJU_MCP_PORT` | `127.0.0.1` / `8000` | HTTP 监听地址和端口 |
缓存保存解析后的 JSON 快照,过期记录在后续写入时清理;不默认保存原始 HTML。支持 HTTPX 的标准代理环境变量,详见 [HTTPX 配置](https://www.python-httpx.org/environment_variables/)。
## 开发与验证
```powershell
uv sync --locked
uv run ruff check src tests
uv run ruff format --check src tests
uv run pytest -q
```
默认测试不访问源站,包含五位教师的页面样本、两种模板、表格保留、签名参数、错误映射、缓存、续读、真实 stdio 子进程及本机 HTTP MCP 协议测试。
低频执行真实网络测试:
```powershell
uv run pytest -q -m live
```
该测试通过实际 stdio MCP 客户端验证匿名搜索、全文搜索、空结果、两种主页模板、单栏目读取及林小杰主页全部五个栏目,需要网络连接源站。
```text
src/zju_teacher_mcp/
server.py MCP 工具、生命周期和启动入口
service.py 实时/缓存策略、组合结果和快照续读
adapter.py 匿名搜索签名、HTTP 请求和栏目加载
parsers.py URL 校验、基本信息与正文解析
models.py 数据模型和领域错误
cache.py SQLite 快照
settings.py 环境配置
tests/ 离线测试、协议测试和可选网络测试
docs/ 产品方案和连接验证记录
```
## 适配边界
当前实现针对已验证的网站 HTML 和公开前端接口。接口没有已确认的对外稳定性承诺;模板或前端签名流程发生变化时,服务会明确返回适配错误。首版使用纯 HTTP,没有引入浏览器运行时、向量检索或自动导师评分。
初始设计见 [产品目标与技术方案](docs/product-and-technical-plan.md),前期网络验证见 [匿名访问实测记录](docs/connectivity-check.md)。
## 许可证与第三方内容
本项目原创代码和文档采用 [MIT 许可证](LICENSE),版权声明为 `Copyright (c) 2026 SummerZhouZihan`。
第三方依赖遵循各自许可证。教师主页内容、图片,以及 `tests/fixtures/` 下的 HTML / JSON 网页测试样本不属于本项目 MIT 授权范围,其权利归相应权利人所有;样本来源见 [测试样本说明](tests/fixtures/README.md)。本项目的许可证不改变源站内容的使用条件。