#include "csp/controllers/meta_controller.h" #include "csp/app_state.h" #include "csp/domain/enum_strings.h" #include "csp/domain/json.h" #include "csp/services/problem_gen_runner.h" #include "csp/services/problem_service.h" #include "csp/services/submission_service.h" #include #include #include namespace csp::controllers { namespace { drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code, const std::string& msg) { Json::Value j; j["ok"] = false; j["error"] = msg; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(code); return resp; } Json::Value BuildOpenApiSpec() { Json::Value root; root["openapi"] = "3.1.0"; root["info"]["title"] = "CSP Platform API"; root["info"]["version"] = "1.0.0"; root["info"]["description"] = "CSP 训练平台接口文档,含认证、题库、评测、导入、MCP。"; root["servers"][0]["url"] = "/"; auto& paths = root["paths"]; paths["/api/health"]["get"]["summary"] = "健康检查"; paths["/api/health"]["get"]["responses"]["200"]["description"] = "OK"; paths["/api/v1/auth/login"]["post"]["summary"] = "登录"; paths["/api/v1/auth/register"]["post"]["summary"] = "注册"; paths["/api/v1/problems"]["get"]["summary"] = "题库列表"; paths["/api/v1/problems/{id}"]["get"]["summary"] = "题目详情"; paths["/api/v1/problems/{id}/submit"]["post"]["summary"] = "提交评测"; paths["/api/v1/problems/{id}/draft"]["get"]["summary"] = "读取代码草稿"; paths["/api/v1/problems/{id}/draft"]["put"]["summary"] = "保存代码草稿"; paths["/api/v1/problems/{id}/solutions"]["get"]["summary"] = "题解列表/任务状态"; paths["/api/v1/problems/{id}/solutions/generate"]["post"]["summary"] = "异步生成题解"; paths["/api/v1/run/cpp"]["post"]["summary"] = "C++ 试运行"; paths["/api/v1/submissions"]["get"]["summary"] = "提交记录"; paths["/api/v1/submissions/{id}"]["get"]["summary"] = "提交详情"; paths["/api/v1/import/jobs/latest"]["get"]["summary"] = "最新导入任务"; paths["/api/v1/import/jobs/run"]["post"]["summary"] = "触发导入任务"; paths["/api/v1/problem-gen/status"]["get"]["summary"] = "CSP-J 生成任务状态"; paths["/api/v1/problem-gen/run"]["post"]["summary"] = "触发生成新题(RAG+去重)"; paths["/api/v1/mcp"]["post"]["summary"] = "MCP JSON-RPC 入口"; auto& components = root["components"]; components["securitySchemes"]["bearerAuth"]["type"] = "http"; components["securitySchemes"]["bearerAuth"]["scheme"] = "bearer"; components["securitySchemes"]["bearerAuth"]["bearerFormat"] = "JWT"; return root; } Json::Value BuildMcpError(const Json::Value& id, int code, const std::string& message) { Json::Value out; out["jsonrpc"] = "2.0"; out["id"] = id; out["error"]["code"] = code; out["error"]["message"] = message; return out; } Json::Value BuildMcpOk(const Json::Value& id, const Json::Value& result) { Json::Value out; out["jsonrpc"] = "2.0"; out["id"] = id; out["result"] = result; return out; } Json::Value McpToolsList() { Json::Value tools(Json::arrayValue); Json::Value t1; t1["name"] = "health"; t1["description"] = "Get backend health"; tools.append(t1); Json::Value t2; t2["name"] = "list_problems"; t2["description"] = "List problems with filters"; tools.append(t2); Json::Value t3; t3["name"] = "get_problem"; t3["description"] = "Get problem by id"; tools.append(t3); Json::Value t4; t4["name"] = "run_cpp"; t4["description"] = "Run C++ code with input"; tools.append(t4); Json::Value t5; t5["name"] = "generate_cspj_problem"; t5["description"] = "Trigger RAG-based CSP-J problem generation"; tools.append(t5); return tools; } } // namespace void MetaController::openapi( const drogon::HttpRequestPtr& /*req*/, std::function&& cb) { auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildOpenApiSpec()); resp->setStatusCode(drogon::k200OK); cb(resp); } void MetaController::mcp( const drogon::HttpRequestPtr& req, std::function&& cb) { try { const auto json = req->getJsonObject(); if (!json) { auto resp = drogon::HttpResponse::newHttpJsonResponse( BuildMcpError(Json::nullValue, -32700, "body must be json")); resp->setStatusCode(drogon::k400BadRequest); cb(resp); return; } const Json::Value id = (*json).isMember("id") ? (*json)["id"] : Json::nullValue; const std::string method = (*json).get("method", "").asString(); const Json::Value params = (*json).isMember("params") ? (*json)["params"] : Json::Value(); if (method == "initialize") { Json::Value result; result["server_name"] = "csp-platform-mcp"; result["server_version"] = "1.0.0"; result["capabilities"]["tools"] = true; auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } if (method == "tools/list") { Json::Value result; result["tools"] = McpToolsList(); auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } if (method != "tools/call") { auto resp = drogon::HttpResponse::newHttpJsonResponse( BuildMcpError(id, -32601, "method not found")); cb(resp); return; } const std::string tool_name = params.get("name", "").asString(); const Json::Value args = params.isMember("arguments") ? params["arguments"] : Json::Value(); if (tool_name == "health") { Json::Value result; result["ok"] = true; result["service"] = "csp-backend"; auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } if (tool_name == "list_problems") { services::ProblemQuery q; q.q = args.get("q", "").asString(); q.page = std::max(1, args.get("page", 1).asInt()); q.page_size = std::max(1, std::min(100, args.get("page_size", 20).asInt())); q.source_prefix = args.get("source_prefix", "").asString(); q.difficulty = std::max(0, std::min(10, args.get("difficulty", 0).asInt())); services::ProblemService svc(csp::AppState::Instance().db()); const auto rows = svc.List(q); Json::Value items(Json::arrayValue); for (const auto& row : rows.items) { items.append(domain::ToJson(row)); } Json::Value result; result["items"] = items; result["total_count"] = rows.total_count; auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } if (tool_name == "get_problem") { const int64_t problem_id = args.get("problem_id", 0).asInt64(); services::ProblemService svc(csp::AppState::Instance().db()); const auto p = svc.GetById(problem_id); if (!p.has_value()) { auto resp = drogon::HttpResponse::newHttpJsonResponse( BuildMcpError(id, -32004, "problem not found")); cb(resp); return; } Json::Value result = domain::ToJson(*p); auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } if (tool_name == "run_cpp") { const std::string code = args.get("code", "").asString(); const std::string input = args.get("input", "").asString(); services::SubmissionService svc(csp::AppState::Instance().db()); const auto r = svc.RunOnlyCpp(code, input); Json::Value result; result["status"] = domain::ToString(r.status); result["time_ms"] = r.time_ms; result["stdout"] = r.stdout_text; result["stderr"] = r.stderr_text; result["compile_log"] = r.compile_log; auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } if (tool_name == "generate_cspj_problem") { const int count = std::max(1, std::min(5, args.get("count", 1).asInt())); const bool started = services::ProblemGenRunner::Instance().TriggerAsync("mcp", count); Json::Value result; result["started"] = started; result["count"] = count; result["running"] = services::ProblemGenRunner::Instance().IsRunning(); auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result)); cb(resp); return; } auto resp = drogon::HttpResponse::newHttpJsonResponse( BuildMcpError(id, -32602, "unknown tool")); cb(resp); } catch (const std::runtime_error& e) { auto resp = drogon::HttpResponse::newHttpJsonResponse( BuildMcpError(Json::nullValue, -32000, e.what())); resp->setStatusCode(drogon::k400BadRequest); cb(resp); } catch (const std::exception& e) { auto resp = drogon::HttpResponse::newHttpJsonResponse( BuildMcpError(Json::nullValue, -32000, e.what())); resp->setStatusCode(drogon::k500InternalServerError); cb(resp); } } } // namespace csp::controllers