258 lines
5.4 KiB
Markdown
258 lines
5.4 KiB
Markdown
# 开发指南
|
||
|
||
本文档介绍如何开发和扩展 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)
|
||
```
|