accoding-mcp-server-new
v0.1.6
Published
MCP Server for Accoding API-test
Downloads
126
Maintainers
Readme
Accoding MCP Server
为北京航空航天大学 Accoding 在线编程平台开发的 Model Context Protocol (MCP) 服务器,提供完整的题目搜索、提交管理、用户信息查询等功能,支持 AI 智能体通过标准化接口与平台交互。
✨ 特性
- 完整的 MCP 协议支持:基于
@modelcontextprotocol/sdk实现标准 MCP 服务器 - 双传输模式:支持 stdio(本地开发)和 HTTP(服务器部署)两种传输方式
- 会话隔离:HTTP 模式下每个连接独立的会话和服务实例
- 分层架构:清晰的 API → Service → Tool 分层设计,易于扩展和维护
- 类型安全:使用 TypeScript 和 Zod 进行完整的类型检查和参数验证
- 智能标签转换:自动将用户友好的标签名称转换为 API 所需的标签 ID
📁 项目结构
accoding-mcp-server/
├── src/
│ ├── common/ # 通用基础层
│ │ ├── constants.ts # 常量定义(语言、分类、标签等)
│ │ └── registry-base.ts # 注册基类(认证/非认证组件管理)
│ ├── accoding/ # Accoding 服务层
│ │ ├── api/ # RESTful API 调用层
│ │ │ ├── problem-api.ts # 题目相关 API
│ │ │ ├── user-api.ts # 用户相关 API
│ │ │ ├── contest-api.ts # 比赛相关 API
│ │ │ ├── class-api.ts # 班级相关 API(题目/考试列表、班级信息等)
│ │ │ └── submission-api.ts # 提交相关 API
│ │ ├── accoding-base-service.ts # 服务接口定义
│ │ ├── accoding-service-factory.ts # 服务工厂类
│ │ └── accoding-service-impl.ts # 服务实现(业务逻辑)
│ ├── mcp/ # MCP 工具和资源层
│ │ ├── tools/ # MCP 工具注册
│ │ │ ├── tool-registry.ts # 工具注册基类
│ │ │ ├── problem-tools.ts # 题目工具
│ │ │ ├── user-tools.ts # 用户与班级相关工具(含班级题目/班级考试列表)
│ │ │ ├── platform-tools.ts # 平台静态信息工具(无需登录)
│ │ │ ├── class-tools.ts # 班级信息工具(课程介绍等,无需登录)
│ │ │ ├── contest-tools.ts # 比赛/考试工具
│ │ │ └── submission-tools.ts # 提交工具
│ │ └── resources/ # MCP 资源注册
│ │ ├── resource-registry.ts # 资源注册基类
│ │ ├── problem-resources.ts # 题目资源
│ │ └── contest-resources.ts # 比赛资源
│ ├── transport/ # 传输层
│ │ ├── http-server.ts # HTTP 服务器实现
│ │ └── mcp-server-factory.ts # MCP Server 创建工厂
│ ├── utils/
│ │ ├── logger.ts # 日志工具(pino)
│ │ └── http-utils.ts # HTTP 工具函数(CORS、cookie解析)
│ └── index.ts # 程序入口
├── package.json
├── tsconfig.json
├── ecosystem.config.cjs # PM2 配置文件
└── README.md🏗️ 架构设计
分层架构
项目采用清晰的分层架构,从下到上分为:
- API 层 (
accoding/api/):封装 Accoding 平台的 RESTful API 调用 - 服务层 (
accoding/):实现业务逻辑,处理数据转换和聚合 - 工具层 (
mcp/tools/):注册 MCP 工具,定义 AI 可调用的接口 - 传输层 (
transport/):处理 MCP 协议的传输(stdio/HTTP)
设计模式
- 工厂模式:
AccodingServiceFactory创建服务实例 - 注册模式:
RegistryBase统一管理工具和资源的注册,根据认证状态动态注册 - 接口隔离:
AccodingBaseService定义服务接口,AccodingServiceImpl实现具体逻辑
认证机制
- stdio 模式:通过
--session参数传递 Accoding session cookie - HTTP 模式:通过
X-Accoding-Sessionheader 或Cookieheader 传递 - 会话隔离:每个 HTTP 连接都有独立的会话 ID 和服务实例
🚀 快速开始
安装依赖
npm install编译项目
npm run build运行服务器
stdio 模式(默认,用于本地开发)
node build/index.js --session "your-accoding-session-cookie"HTTP 模式(用于服务器部署)
node build/index.js --mode http --port 3000命令行参数
--session, -s <cookie> Accoding平台会话Cookie(stdio模式使用)
--mode, -m <mode> 传输模式: stdio(默认)或 http
--port, -p <port> HTTP模式端口号(默认: 3000)
--help, -h 显示帮助信息💻 代码示例
API 层示例
API 层负责封装 HTTP 请求,处理参数构建和响应解析:
// src/accoding/api/problem-api.ts
const API_BASE_URL = "http://8.141.100.178:8000";
async function apiRequest(
endpoint: string,
options: RequestInit = {},
session?: string
): Promise<any> {
const url = `${API_BASE_URL}${endpoint}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>)
};
if (session) {
headers["Authorization"] = session;
}
const response = await fetch(url, { ...options, headers });
// ... 错误处理和响应解析
}
export async function searchProblems(
page: number,
limit: number,
titleSearch?: string,
tagSearch?: number[],
session?: string
): Promise<any> {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("limit", limit.toString());
if (titleSearch) {
params.append("titleSearch", titleSearch);
}
// tagSearch 是 int 数组,使用重复参数格式
if (tagSearch && tagSearch.length > 0) {
tagSearch.forEach((tagId) => {
params.append("tagSearch", tagId.toString());
});
}
const endpoint = `/api/problem/search?${params.toString()}`;
const response = await apiRequest(endpoint, { method: "GET" }, session);
if (response.status !== 200) {
throw new Error(`搜索题目失败: ${response.msg || "未知错误"}`);
}
return response.data || [];
}服务层示例
服务层实现业务逻辑,如标签名称到 ID 的转换、自动分页等:
// src/accoding/accoding-service-impl.ts
export class AccodingServiceImpl implements AccodingBaseService {
private session?: string;
async searchProblems(
titleSearch?: string,
idSearch?: string,
authorSearch?: string,
tagNames?: string[]
): Promise<any> {
const limit = 10; // 固定每页10道题目
// 标签名称转 ID
let tagIds: number[] | undefined;
if (tagNames && tagNames.length > 0) {
const allTags = await problemApi.getAllTags(this.session);
const tagMap = new Map<string, number>();
allTags.forEach((tag: any) => {
tagMap.set(tag.name, tag.id);
});
tagIds = tagNames
.map((name) => tagMap.get(name))
.filter((id): id is number => id !== undefined);
}
// 获取总页数
const totalPages = await problemApi.getProblemSearchPageNum(
limit, titleSearch, idSearch, authorSearch, tagIds, this.session
);
// 自动获取所有页面(最多20页)
const MAX_PAGES = 20;
const pagesToFetch = Math.min(totalPages, MAX_PAGES);
const allProblems: any[] = [];
for (let page = 1; page <= pagesToFetch; page++) {
const problems = await problemApi.searchProblems(
page, limit, titleSearch, idSearch, authorSearch, tagIds, this.session
);
allProblems.push(...problems);
}
// 添加链接和状态说明
const problemsWithLinks = allProblems.map((problem: any) => ({
...problem,
link: `http://8.141.100.178:8000/problem/${problem.id}`,
statusText: statusMap[problem.status],
difficultyText: `${problem.difficulty}颗星`
}));
return {
problems: problemsWithLinks,
totalPages,
fetchedPages: pagesToFetch,
isTruncated: totalPages > MAX_PAGES
};
}
}工具注册示例
工具层使用 Zod 定义参数模式,注册 MCP 工具:
// src/mcp/tools/problem-tools.ts
import { z } from "zod";
import { ToolRegistry } from "./tool-registry.js";
export class ProblemToolRegistry extends ToolRegistry {
protected registerAuthenticated(): void {
this.registerSearchProblems();
this.registerGetProblemInfo();
}
private registerSearchProblems(): void {
this.server.tool(
"search_problems",
"搜索题目列表,支持按标题、ID、作者、标签等条件搜索。",
{
titleSearch: z.string().optional()
.describe("题目标题模糊搜索(可选)"),
idSearch: z.string().optional()
.describe("题目ID精确搜索(可选)"),
authorSearch: z.string().optional()
.describe("创建者的nickname模糊搜索(可选)"),
tagNames: z.array(z.string()).optional()
.describe("按标签搜索题目,传入标签名称数组。例如:['判断', '二叉树']")
},
async ({ titleSearch, idSearch, authorSearch, tagNames }) => {
try {
const result = await this.accodingService.searchProblems(
titleSearch, idSearch, authorSearch, tagNames
);
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
} catch (error: any) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: "Failed to search problems",
message: error.message
})
}],
isError: true
};
}
}
);
}
}注册基类示例
使用注册基类管理认证/非认证组件的动态注册:
// src/common/registry-base.ts
export abstract class RegistryBase {
constructor(
protected server: McpServer,
protected accodingService: AccodingBaseService
) {}
protected isAuthenticated(): boolean {
return this.accodingService.isAuthenticated();
}
public register(): void {
this.registerCommon(); // 注册通用组件
if (this.isAuthenticated()) {
this.registerAuthenticated(); // 注册需要认证的组件
}
}
protected registerCommon(): void {}
protected registerAuthenticated(): void {}
}📦 已实现功能
当前 MCP 工具共 19 个。注册逻辑见各 src/mcp/tools/*-tools.ts;RegistryBase 会先注册「无需登录」工具,若当前会话带有效 Authorization(已登录),再注册「需要登录」工具。
以下为每个工具的功能说明(按调用场景组织)。
无需登录(2)
get_platform_info
不请求远端接口。返回 Accoding 平台静态介绍:开发团队名单、推荐教材与购书链接、联系邮箱等,供助手回答「关于平台 / 教材 / 联系方式」类问题。
get_class_info
根据 classId 获取单个班级的课程介绍面板数据。优先请求管理端接口,失败则回退到公开接口;不传 Authorization。返回中通常包含课程名称、所属类别、课程简介等(具体字段以接口 data 为准),并附带 raw 便于排查字段差异。
需要登录(17)
题目(problem-tools.ts)
search_problems
支持按题目标题模糊检索、按题目 ID精确检索、按创建者昵称检索、按标签名称数组检索(内部会把标签名转为标签 ID)。会先取总页数,再按页拉取并合并结果(最多 20 页,每页 10 道题)。返回每道题含题目链接、作答状态说明(已通过 / 未通过 / 未做)、难度星级文案等聚合信息。
get_problem_info
根据题目 ID 获取单题详情:题目内容、时间/内存限制(含格式化说明)、提交与通过统计、支持的语言列表;并计算通过率、正确率等可读指标及题目直达链接。
用户与班级(user-tools.ts)
get_user_info
获取当前登录用户档案:昵称、邮箱、学校、学号、专业等(以接口返回为准)。
get_class_list
支持按班级名称、课程名称筛选;两参数都省略时相当于拉取班级列表。返回每条班级含 status 及 statusText:1 表示课程进行中,0 表示已结束;并返回 statusDescription 供模型理解枚举含义。
get_my_class_list
获取当前用户已加入的班级列表,字段含义与 get_class_list 类似,同样附带 statusText 与 statusDescription。
get_class_problem_list
根据 classId 获取该班级下的题目列表(默认按后端分页一次拉满)。返回每道题含题目 ID、标题、难度、标签等;并增加题目访问链接、题目 status 的 statusText(如可用/禁用)及解析后的 tagNames。
get_class_contest_list
根据 classId 获取该班级下的考试/比赛列表(多页合并策略与比赛列表工具一致)。返回每场考试含 examId、名称、时间、题目数等;并增加考试首页 link、考试进度字段 stat 的 statText(未开始 / 进行中 / 已结束)。
get_user_submission_stats
获取当前用户在平台上的提交结果汇总:各评测结果(如 AC、CE、WA 等)对应的数量,并附带结果缩写的人类可读说明。
get_user_weekly_submissions
获取最近一周(按接口约定)每天的提交分布;返回中会把原始数组格式化为「星期几 + 各状态数量」等便于阅读的结构,并保留 rawData 供核对。
比赛 / 考试(contest-tools.ts)
list_contests
支持按比赛名称、按所在班级名称(对应后端 AuthorSearch)筛选。先取总页数,再分页拉取并合并(最多 20 页,每页条数可配置上限)。返回每场含 examId、时间、状态等,并含状态文案与考试首页链接。
get_contest_exam_info
根据 examId 获取单场考试/比赛的基础信息:名称、开始/结束时间、描述等;并附带考试首页链接与原始 data,便于计算剩余时间等。
get_contest_exam_problems_and_status
并行获取该考试的题目列表与当前用户对每道题的提交状态列表,按题目顺序合并为一条记录:每题含题目内容与统计、语言限制等,以及 userStatus / userStatusText(如未提交、已通过、提交但错误);并含题目直达链接。
get_contest_exam_rank_list
根据 examId 获取该考试的排名榜(顺序、学号、昵称、得分、每题罚时等,以接口为准)。
get_contest_exam_submissions
获取当前登录用户在该考试下的个人提交记录(不是全员排行榜里的所有人提交)。内部固定 pageSize=20,从第 1 页依次请求到最后一页并拼接为完整列表。可选 filter:扁平对象,键名与前端 SubmissionList 的 searchFilter 一致(如 submissionId、problemId、nickname、result),会作为 Query 参数与 page / pageSize 一并传递。返回含 totalPages、fetchedPages、每条记录的跳转路径、题目链接、评测结果文案,以及 note 强调数据范围仅为当前用户。
提交(submission-tools.ts)
get_problem_submissions
根据题目 ID 获取该题的全部提交记录(可先取总页数再逐页合并)。支持按评测结果、提交 ID 等过滤(与工具参数一致)。返回每条含结果的人类可读说明。
get_submission_detail
根据提交记录 ID 同时拉取评测详情与提交代码;含时间/内存格式化和题目链接等。
submit_code
向指定题目提交代码:传入题目 ID、语言名称(内部映射为语言 ID)、代码正文;可选提交时间与题目时间(缺省为当前时间)。返回提交结果摘要。
说明:源码中仍存在
get_contest_problems的注册函数,但默认未在registerCommon/registerAuthenticated中启用,因此不计入上述 19 个对外工具。
🔧 开发指南
添加新工具
- 在 API 层添加 API 调用函数 (
src/accoding/api/)
// src/accoding/api/xxx-api.ts
export async function getXxx(params: any, session?: string): Promise<any> {
const response = await apiRequest("/api/xxx", { method: "GET" }, session);
if (response.status !== 200) {
throw new Error(`获取失败: ${response.msg}`);
}
return response.data;
}- 在服务接口中添加方法 (
src/accoding/accoding-base-service.ts)
export interface AccodingBaseService {
getXxx(params: any): Promise<any>;
}- 在服务实现中实现方法 (
src/accoding/accoding-service-impl.ts)
async getXxx(params: any): Promise<any> {
return await xxxApi.getXxx(params, this.session);
}- 在工具注册器中注册工具 (
src/mcp/tools/xxx-tools.ts)
export class XxxToolRegistry extends ToolRegistry {
protected registerAuthenticated(): void {
this.server.tool(
"get_xxx",
"工具描述",
{
param: z.string().describe("参数说明")
},
async ({ param }) => {
const result = await this.accodingService.getXxx(param);
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
);
}
}- 在 MCP Server 工厂中注册工具 (
src/transport/mcp-server-factory.ts)
import { registerXxxTools } from "../mcp/tools/xxx-tools.js";
export function createMCPServer(accodingService: AccodingBaseService): McpServer {
// ...
registerXxxTools(server, accodingService);
// ...
}开发模式
使用 tsc-watch 进行开发,代码变更自动重新编译:
npm run dev代码格式化
npm run format🚢 部署说明
PM2 部署
项目已配置 PM2 配置文件 ecosystem.config.cjs,可直接使用 PM2 管理进程:
# 启动服务
pm2 start ecosystem.config.cjs
# 查看状态
pm2 status
# 查看日志
pm2 logs accoding-mcp
pm2 logs accoding-mcp --lines 50
# 重启应用
pm2 restart accoding-mcp
# 停止应用
pm2 stop accoding-mcp
# 删除应用
pm2 delete accoding-mcp
# 监控
pm2 monit
# 重新加载(代码更新后,零停机重启)
pm2 reload accoding-mcp开机自启动
# 1. 保存当前 PM2 配置
pm2 save
# 2. 添加到 crontab
(crontab -l 2>/dev/null; echo "@reboot cd /path/to/accoding_mcp_server && pm2 resurrect") | crontab -
# 3. 验证添加成功
crontab -lHTTP 模式使用示例
初始化连接
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "X-Accoding-Session: your-session-cookie" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "initialize",
"params": {}
}'响应会包含 mcp-session-id header,后续请求需要使用此 ID。
调用工具
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "mcp-session-id: <从初始化响应中获取的session-id>" \
-H "X-Accoding-Session: your-session-cookie" \
-d '{
"jsonrpc": "2.0",
"id": "2",
"method": "tools/call",
"params": {
"name": "search_problems",
"arguments": {
"tagNames": ["判断"]
}
}
}'会话隔离
- 每个 HTTP 连接都有独立的会话 ID
- 每个会话都有独立的
AccodingService实例 - 不同客户端的 session cookie 互不影响
- 连接关闭后自动清理会话资源
📝 环境变量
ACCODING_SESSION: Accoding 会话 Cookie(stdio 模式使用,HTTP 模式从请求头获取)
注意:本项目为北京航空航天大学 Accoding 平台的 MCP 服务器实现,仅供学习和研究使用。
