diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..f66913e --- /dev/null +++ b/SKILL.md @@ -0,0 +1,264 @@ +--- +name: educoder-mcp +description: 提供 Educoder 课堂相关查询能力,当前支持课堂列表、课堂作业列表、作业学生列表、课堂考试列表、考试学生列表查询。 +--- + +## MCP 服务地址 + +- 本地开发:`http://localhost:8001/mcp` + +> 使用 `streamable-http` 模式连接,不使用 SSE。 + +## 接入方式 + +- 客户端应使用 `streamable-http` MCP 客户端 +- 不要使用 `sse_client` +- 服务端口默认是 `8001` +- MCP 路径默认是 `/mcp` + +## 前置配置 + +接入 OpenClaw 时,需要让用户提供 `Authorization`,并在 MCP 客户端建立连接时放到请求头里。 + +### 单用户本地调试 + +本地单用户调试时,可以临时使用环境变量保存: + +```bash +export EDUCODER_AUTHORIZATION="Bearer xxxxx" +``` + +> 这种方式只适合本地单用户调试,不适合多用户线上场景。 + +MCP 客户端连接时应注入: + +```python +headers = { + "Authorization": os.getenv("EDUCODER_AUTHORIZATION", "").strip() +} +``` + +然后在创建 MCP 连接时透传: + +```python +async with streamable_http_client(self.server_url, headers=headers or None) as (read, write, _): + yield read, write +``` + +> `Authorization` 走 MCP 连接层请求头,不放在工具参数里。 + +### 多用户隔离 + +多用户场景下,不要把 `Authorization` 放在服务进程级环境变量里,否则所有用户会共用同一份认证信息。 + +正确做法是: + +1. 让每个用户单独输入自己的 `Authorization` +2. 调用端把这个值绑定到当前用户会话 +3. 创建当前用户专属的 MCPClient 实例 +4. 在 MCP 建连时,把当前用户自己的 `Authorization` 放到请求头里 + +示例: + +```python +client = MCPClient( + server_url=tool_server_url, + authorization=current_user_authorization, +) +``` + +然后在建连时动态透传: + +```python +headers = {} +if self.authorization: + headers["Authorization"] = self.authorization + +async with streamable_http_client(self.server_url, headers=headers or None) as (read, write, _): + yield read, write +``` + +> 核心原则:`Authorization` 必须按用户动态传递,不能做成所有人共享的全局配置。 + +### OpenClaw 接入建议 + +如果使用 OpenClaw,推荐按下面的方式接入: + +1. 用户在界面上输入自己的 `Authorization` +2. OpenClaw 后端把该值保存到当前用户自己的会话上下文 +3. 当前用户发起工具调用时,后端读取这个用户自己的 `Authorization` +4. 用这个值创建当前用户专属的 MCPClient +5. MCPClient 建连时把 `Authorization` 放进 MCP headers + +参考流程: + +```python +current_user_authorization = session_store.get(current_user_id, "educoder_authorization") + +client = MCPClient( + server_url=tool_server_url, + authorization=current_user_authorization, +) + +tool_result = await client.call_tool(tool_name, tool_arguments) +``` + +MCPClient 示例: + +```python +class MCPClient: + def __init__(self, server_url: str, authorization: str | None = None): + self.server_url = server_url + self.authorization = authorization + + def _build_headers(self) -> dict[str, str]: + headers = {} + if self.authorization: + headers["Authorization"] = self.authorization + return headers +``` + +不推荐的做法: + +- 把 `Authorization` 写进服务端 `.env` +- 把 `Authorization` 写成所有用户共享的全局变量 +- 让多个用户复用同一个 MCPClient 实例 + +## 触发场景 + +### 意图 → 工具 速查表 + +| 用户意图 | 调用工具 | +|----------|---------| +| 查询当前用户的课堂列表 | `educoder_user_course_list` | +| 查询某个课堂中的作业列表 | `educoder_course_homeworks` | +| 查询某次作业下的学生列表 | `educoder_homework_student_works` | +| 查询某个课堂中的考试列表 | `educoder_course_exercises` | +| 查询某场考试下的学生列表 | `educoder_exercise_users` | + +### 不要触发的情况 + +- 与课堂、作业、考试数据查询无关的问题 +- 需要新增、修改、删除数据的写操作 +- 纯闲聊场景 + +### 组合调用示例 + +> 用户说:“帮我查一下我有哪些课堂,再看看某个课堂里的作业和考试” + +依次调用: +1. `educoder_user_course_list` +2. `educoder_course_homeworks` +3. `educoder_course_exercises` + +> 用户说:“帮我看一下这次作业和这场考试的学生列表” + +依次调用: +1. `educoder_homework_student_works` +2. `educoder_exercise_users` + +--- + +## 工具详细说明 + +### 全局规则 + +- 当前工具都是查询类工具,没有写操作 +- 调用时必须传入完整的结构化 JSON 参数对象 +- 参数名必须严格匹配工具定义 +- 当前查询逻辑还是占位实现,后续会接外部 HTTP 接口 +- `course_id` 和 `exercise_id` 当前按字符串传入 +- 客户端连接 MCP 服务时,请在 transport headers 中传 `Authorization` + +--- + +### 1. `educoder_user_course_list` + +获取当前用户课堂列表。 + +**参数:** 无 + +**返回字段:** +- `status` +- `message` +- `data` +- `size` + +--- + +### 2. `educoder_course_homeworks` + +获取课堂中的作业列表。 + +**参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `course_id` | 是 | 课堂 ID | + +--- + +### 3. `educoder_homework_student_works` + +获取某次作业下的学生列表。 + +**参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `exercise_id` | 是 | 作业 ID | + +--- + +### 4. `educoder_course_exercises` + +获取课堂中的考试列表。 + +**参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `course_id` | 是 | 课堂 ID | + +--- + +### 5. `educoder_exercise_users` + +获取某场考试下的学生列表。 + +**参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `exercise_id` | 是 | 考试 ID | + +--- + +## 客户端调用示例 + +```python +import httpx +from mcp import ClientSession +from mcp.client.streamable_http import streamable_http_client + + +async def main(): + async with httpx.AsyncClient(headers={"Authorization": "Bearer demo-token"}) as http_client: + async with streamable_http_client("http://127.0.0.1:8001/mcp", http_client=http_client) as (read, write, _): + async with ClientSession(read, write) as session: + await session.initialize() + + tools = await session.list_tools() + print([tool.name for tool in tools.tools]) + + result = await session.call_tool("educoder_user_course_list", {}) + print(result) +``` + +## 注意事项 + +- 当前服务使用 `streamable-http`,不是 SSE +- 如果客户端使用了 `sse_client`,将无法正常调用当前服务 +- `Authorization` 应通过 MCP 连接层 headers 传入,不放在工具参数里 +- 当前工具返回空 `data` 数组属于预期行为,因为外部接口尚未接入 +- 后续新增或调整工具时,需要同步更新本文件