199 lines
7.4 KiB
Java
199 lines
7.4 KiB
Java
package com.cloudhandson.vpdbackoffice.service;
|
|
|
|
import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import java.util.List;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
/** MCP boundary exposing only the row-access-aware GPT-5.4-mini Select AI query tool. */
|
|
@Service
|
|
public class McpSseService {
|
|
|
|
private static final String SELECT_AI_VPD_QUERY_TOOL = "ords.query.kb_select_ai_vpd";
|
|
private static final String SELECT_AI_VPD_QUERY_PATH = "cb-ords/kb-select-ai-vpd/query";
|
|
private static final String SELECT_AI_VPD_QUERY_PROFILE =
|
|
"KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1";
|
|
private static final McpToolView SELECT_AI_VPD_QUERY_VIEW = new McpToolView(
|
|
SELECT_AI_VPD_QUERY_TOOL,
|
|
"GPT-5.4-mini Select AI 자연어 질의를 행 접근 컨텍스트로 실행합니다. 테이블/컬럼 comment, annotation, constraint 메타데이터를 사용하고 생성 SQL은 KB 업무 테이블의 읽기 전용 SELECT/WITH만 허용합니다.",
|
|
-1L,
|
|
"KB Select AI 행 접근 자연어 조회",
|
|
SELECT_AI_VPD_QUERY_PATH
|
|
);
|
|
|
|
private final SelectAiAgentOrdsService selectAiAgentOrdsService;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
public McpSseService(
|
|
SelectAiAgentOrdsService selectAiAgentOrdsService,
|
|
ObjectMapper objectMapper
|
|
) {
|
|
this.selectAiAgentOrdsService = selectAiAgentOrdsService;
|
|
this.objectMapper = objectMapper;
|
|
}
|
|
|
|
public ObjectNode handle(String contextPath, JsonNode request) {
|
|
return handle(contextPath, request, "");
|
|
}
|
|
|
|
/**
|
|
* The HTTP bearer token is the business-user subject token; no separate MCP token is used.
|
|
*/
|
|
public ObjectNode handle(String contextPath, JsonNode request, String vpdBearerToken) {
|
|
ObjectNode response = objectMapper.createObjectNode();
|
|
response.put("jsonrpc", "2.0");
|
|
if (request != null && request.has("id")) {
|
|
response.set("id", request.get("id"));
|
|
}
|
|
|
|
String method = request == null || !request.hasNonNull("method") ? "" : request.get("method").asText();
|
|
JsonNode parameters = request == null ? objectMapper.createObjectNode() : request.path("params");
|
|
try {
|
|
response.set("result", switch (method) {
|
|
case "initialize" -> initializeResult(contextPath);
|
|
case "notifications/initialized" -> objectMapper.createObjectNode();
|
|
case "tools/list" -> toolsListResult();
|
|
case "tools/call" -> toolsCallResult(parameters, vpdBearerToken);
|
|
default -> throw new AppException("지원하지 않는 MCP method입니다: " + method);
|
|
});
|
|
} catch (Exception e) {
|
|
response.remove("result");
|
|
ObjectNode error = objectMapper.createObjectNode();
|
|
error.put("code", -32000);
|
|
error.put("message", e.getMessage());
|
|
response.set("error", error);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/** The only tool registered by this MCP server. */
|
|
public List<McpToolView> registeredTools() {
|
|
return List.of(SELECT_AI_VPD_QUERY_VIEW);
|
|
}
|
|
|
|
private ObjectNode initializeResult(String contextPath) {
|
|
ObjectNode result = objectMapper.createObjectNode();
|
|
result.put("protocolVersion", "2024-11-05");
|
|
ObjectNode serverInfo = objectMapper.createObjectNode();
|
|
serverInfo.put("name", "vpd-ords-backoffice-" + contextPath);
|
|
serverInfo.put("version", "0.1.0");
|
|
result.set("serverInfo", serverInfo);
|
|
ObjectNode capabilities = objectMapper.createObjectNode();
|
|
capabilities.set("tools", objectMapper.createObjectNode());
|
|
result.set("capabilities", capabilities);
|
|
return result;
|
|
}
|
|
|
|
private ObjectNode toolsListResult() {
|
|
ObjectNode result = objectMapper.createObjectNode();
|
|
ArrayNode tools = objectMapper.createArrayNode();
|
|
tools.add(selectAiVpdQueryTool());
|
|
result.set("tools", tools);
|
|
return result;
|
|
}
|
|
|
|
private ObjectNode selectAiVpdQueryTool() {
|
|
ObjectNode item = objectMapper.createObjectNode();
|
|
item.put("name", SELECT_AI_VPD_QUERY_TOOL);
|
|
item.put("description", SELECT_AI_VPD_QUERY_VIEW.description());
|
|
|
|
ObjectNode schema = objectMapper.createObjectNode();
|
|
schema.put("type", "object");
|
|
ObjectNode properties = objectMapper.createObjectNode();
|
|
|
|
ObjectNode prompt = objectMapper.createObjectNode();
|
|
prompt.put("type", "string");
|
|
prompt.put("description", "KB 업무 원장에 대해 조회할 내용을 자연어로 입력합니다.");
|
|
prompt.put("maxLength", 4000);
|
|
properties.set("prompt", prompt);
|
|
|
|
ObjectNode limit = objectMapper.createObjectNode();
|
|
limit.put("type", "integer");
|
|
limit.put("description", "최대 반환 행 수. 1부터 100까지 허용하며 기본값은 50입니다.");
|
|
limit.put("minimum", 1);
|
|
limit.put("maximum", 100);
|
|
properties.set("limit", limit);
|
|
|
|
schema.set("properties", properties);
|
|
ArrayNode required = objectMapper.createArrayNode();
|
|
required.add("prompt");
|
|
schema.set("required", required);
|
|
schema.put("additionalProperties", false);
|
|
item.set("inputSchema", schema);
|
|
return item;
|
|
}
|
|
|
|
private ObjectNode toolsCallResult(JsonNode params, String vpdBearerToken) {
|
|
String toolName = params.path("name").asText("");
|
|
if (!SELECT_AI_VPD_QUERY_TOOL.equals(toolName)) {
|
|
throw new AppException("등록되지 않은 MCP tool입니다: " + toolName);
|
|
}
|
|
|
|
JsonNode arguments = params.path("arguments");
|
|
String token = vpdBearerToken == null ? "" : vpdBearerToken.trim();
|
|
if (token.isBlank()) {
|
|
return tokenAccessDeniedResult();
|
|
}
|
|
JsonNode response;
|
|
try {
|
|
response = selectAiAgentOrdsService.run(
|
|
token,
|
|
arguments.path("prompt").asText(""),
|
|
normalizeLimit(arguments.path("limit").asInt(50))
|
|
);
|
|
} catch (VpdTokenAccessDeniedException ignored) {
|
|
return tokenAccessDeniedResult();
|
|
}
|
|
|
|
ObjectNode payload = objectMapper.createObjectNode();
|
|
payload.put("toolName", SELECT_AI_VPD_QUERY_TOOL);
|
|
payload.put("profile", SELECT_AI_VPD_QUERY_PROFILE);
|
|
payload.put("ordsPath", SELECT_AI_VPD_QUERY_PATH);
|
|
payload.set("response", response);
|
|
|
|
ObjectNode result = objectMapper.createObjectNode();
|
|
ArrayNode content = objectMapper.createArrayNode();
|
|
ObjectNode text = objectMapper.createObjectNode();
|
|
text.put("type", "text");
|
|
text.put("text", pretty(payload));
|
|
content.add(text);
|
|
result.set("content", content);
|
|
result.put("isError", false);
|
|
return result;
|
|
}
|
|
|
|
private ObjectNode tokenAccessDeniedResult() {
|
|
ObjectNode payload = objectMapper.createObjectNode();
|
|
payload.put("status", "VPD_TOKEN_DENIED");
|
|
payload.put("message", "토큰이 없거나 유효하지 않아 이 요청을 수행할 권한이 없습니다.");
|
|
|
|
ObjectNode result = objectMapper.createObjectNode();
|
|
ArrayNode content = objectMapper.createArrayNode();
|
|
ObjectNode text = objectMapper.createObjectNode();
|
|
text.put("type", "text");
|
|
text.put("text", pretty(payload));
|
|
content.add(text);
|
|
result.set("content", content);
|
|
result.put("isError", true);
|
|
return result;
|
|
}
|
|
|
|
private int normalizeLimit(int limit) {
|
|
if (limit < 1) {
|
|
return 50;
|
|
}
|
|
return Math.min(limit, 100);
|
|
}
|
|
|
|
private String pretty(Object value) {
|
|
try {
|
|
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
|
} catch (Exception e) {
|
|
return String.valueOf(value);
|
|
}
|
|
}
|
|
}
|