init
This commit is contained in:
257
docs/development.md
Normal file
257
docs/development.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 开发指南
|
||||
|
||||
本文档介绍如何开发和扩展 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)
|
||||
```
|
||||
Reference in New Issue
Block a user