|
|
|
|
@ -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` 数组属于预期行为,因为外部接口尚未接入
|
|
|
|
|
- 后续新增或调整工具时,需要同步更新本文件
|