Files
njupt-mcp/docs/development.md
2026-03-31 11:38:32 +08:00

258 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发指南
本文档介绍如何开发和扩展 NJUPT MCP Server。
## 架构概述
```
┌─────────────────┐
│ MCP Client │ (Kimi Code CLI, Claude Desktop, etc.)
│ (Kimi, Claude) │
└────────┬────────┘
│ MCP Protocol (stdio / SSE)
┌─────────────────┐
│ njupt-mcp │
│ (FastMCP) │
└────────┬────────┘
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ Tools │ │Resources│
└───┬───┘ └───┬───┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│ NJUPT │ │ Static│
│ APIs │ │ Data │
└───────┘ └───────┘
```
## 添加新功能
### 1. 添加工具 (Tool)
工具用于执行操作、调用 API。在 `src/njupt_mcp/server.py` 中添加:
```python
@mcp.tool()
async def my_tool(param1: str, param2: int = 10) -> str:
"""工具的描述,会显示给 LLM
详细说明工具的用途、使用场景等。
Args:
param1: 参数1的描述
param2: 参数2的描述默认为 10
Returns:
返回值的描述,通常是 JSON 字符串
Example:
my_tool("test", 20) -> 返回结果
"""
# 实现逻辑
result = {"param1": param1, "param2": param2}
return json.dumps(result, ensure_ascii=False)
```
**最佳实践:**
- 使用 `async def` 定义异步函数
- 提供详细的文档字符串LLM 依赖此信息决定是否调用)
- 使用类型注解
- 返回 JSON 字符串便于 LLM 解析
- 处理异常情况,返回友好的错误信息
### 2. 添加资源 (Resource)
资源用于提供数据、文档等只读内容:
```python
@mcp.resource("njupt://resource-name")
async def get_resource() -> str:
"""资源的描述"""
return "资源内容"
```
资源 URI 格式:`scheme://path`,建议使用 `njupt://` 作为 scheme。
### 3. 添加提示词模板 (Prompt)
提示词模板用于生成特定场景的提示词:
```python
@mcp.prompt()
def my_prompt(context: str, question: str) -> str:
"""提示词模板的描述"""
return f"""基于以下上下文:
{context}
请回答:{question}
"""
```
## 数据获取实现
目前示例代码使用硬编码数据,实际应用中需要对接 NJUPT 的数据源:
### 教务系统
```python
import httpx
async def fetch_course_data(course_code: str) -> dict:
"""从教务系统获取课程数据"""
async with httpx.AsyncClient() as client:
response = await client.get(
"https://jw.njupt.edu.cn/api/course",
params={"code": course_code},
cookies={"session": await get_session()}
)
response.raise_for_status()
return response.json()
```
### 图书馆 OPAC
```python
async def search_library(keyword: str) -> list[dict]:
"""搜索图书馆 OPAC 系统"""
async with httpx.AsyncClient() as client:
response = await client.get(
"http://opac.njupt.edu.cn/opac/search",
params={"q": keyword}
)
# 解析 HTML 或 JSON
return parse_search_results(response.text)
```
## 测试
### 运行测试
```bash
# 运行所有测试
pytest
# 运行特定测试文件
pytest tests/test_helpers.py -v
# 运行并生成覆盖率报告
pytest --cov=njupt_mcp --cov-report=html
```
### 编写测试
```python
# tests/test_new_feature.py
import pytest
from njupt_mcp.server import my_new_tool
@pytest.mark.asyncio
async def test_my_tool():
result = await my_tool("test", 20)
assert "test" in result
assert "20" in result
```
## 调试技巧
### 使用 MCP Inspector
```bash
# 启动服务器
uv run njupt-mcp --transport streamable-http
# 启动 Inspector
npx -y @modelcontextprotocol/inspector
```
访问 http://localhost:5173 进行交互式调试。
### 启用调试日志
```bash
uv run njupt-mcp --debug
```
### 手动测试工具
```python
# test_manual.py
import asyncio
from njupt_mcp.server import search_course
async def main():
result = await search_course("数据结构")
print(result)
if __name__ == "__main__":
asyncio.run(main())
```
## 部署
### 本地部署
使用 `uv` 运行:
```bash
uv run njupt-mcp
```
### Docker 部署
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -e "."
EXPOSE 8000
CMD ["njupt-mcp", "--transport", "sse", "--host", "0.0.0.0"]
```
### 云服务部署
适合使用 SSE 或 streamable-http 模式,部署到:
- [Railway](https://railway.app/)
- [Render](https://render.com/)
- [Fly.io](https://fly.io/)
## 性能优化
### 添加缓存
```python
from functools import lru_cache
import asyncio
from cachetools import TTLCache
# 使用 TTLCache 缓存结果
course_cache = TTLCache(maxsize=100, ttl=3600)
@mcp.tool()
async def search_course(keyword: str) -> str:
if keyword in course_cache:
return course_cache[keyword]
result = await fetch_course_from_api(keyword)
course_cache[keyword] = result
return result
```
### 并发请求
```python
async def fetch_multiple_courses(course_codes: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [fetch_course(client, code) for code in course_codes]
return await asyncio.gather(*tasks)
```