refs #731: expose database game scope MCP

This commit is contained in:
devmrko
2026-07-27 13:43:59 +09:00
parent e77b3e0543
commit b69abe0f3a
5 changed files with 157 additions and 10 deletions

View File

@@ -20,7 +20,10 @@ public record McpProperties(
String qaVectorStoreToolDescription,
String fewShotNl2SqlToolName,
String fewShotNl2SqlToolLabel,
String fewShotNl2SqlToolDescription
String fewShotNl2SqlToolDescription,
String gameScopeToolName,
String gameScopeToolLabel,
String gameScopeToolDescription
) {
private static final String DEFAULT_TOOL_NAME = "oracle.select_ai.data_text2sql";
@@ -56,6 +59,12 @@ public record McpProperties(
private static final String DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION =
"벡터 Few-shot 예제를 찾아 prompt에 반영하고, SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. "
+ "Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.";
private static final String DEFAULT_GAME_SCOPE_TOOL_NAME =
"oracle.select_ai.game_scope_resolve";
private static final String DEFAULT_GAME_SCOPE_TOOL_LABEL = "게임 조회 범위 확인";
private static final String DEFAULT_GAME_SCOPE_TOOL_DESCRIPTION =
"질문에서 언급된 게임 별칭을 DB 게임 범위 view로 확인합니다. 데이터 SQL은 실행하지 않으며, "
+ "반환된 SUPPORTED scope에만 Few-shot NL2SQL을 호출하세요.";
public String resolvedToolName() {
return requiredOrDefault(toolName, DEFAULT_TOOL_NAME);
@@ -121,6 +130,18 @@ public record McpProperties(
return requiredOrDefault(fewShotNl2SqlToolDescription, DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION);
}
public String resolvedGameScopeToolName() {
return requiredOrDefault(gameScopeToolName, DEFAULT_GAME_SCOPE_TOOL_NAME);
}
public String resolvedGameScopeToolLabel() {
return requiredOrDefault(gameScopeToolLabel, DEFAULT_GAME_SCOPE_TOOL_LABEL);
}
public String resolvedGameScopeToolDescription() {
return requiredOrDefault(gameScopeToolDescription, DEFAULT_GAME_SCOPE_TOOL_DESCRIPTION);
}
private String requiredOrDefault(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}

View File

@@ -21,19 +21,22 @@ public class McpSseService {
private final BackofficeProperties properties;
private final McpProperties mcpProperties;
private final QaVectorService qaVectorService;
private final GameScopeService gameScopeService;
public McpSseService(
SelectAiService selectAiService,
ObjectMapper objectMapper,
BackofficeProperties properties,
McpProperties mcpProperties,
QaVectorService qaVectorService
QaVectorService qaVectorService,
GameScopeService gameScopeService
) {
this.selectAiService = selectAiService;
this.objectMapper = objectMapper;
this.properties = properties;
this.mcpProperties = mcpProperties;
this.qaVectorService = qaVectorService;
this.gameScopeService = gameScopeService;
}
public ObjectNode handle(String contextPath, JsonNode request) {
@@ -74,7 +77,7 @@ public class McpSseService {
public List<McpToolView> registeredTools() {
return List.of(
selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(),
qaVectorStoreView(), fewShotNl2SqlView());
qaVectorStoreView(), fewShotNl2SqlView(), gameScopeView());
}
private ObjectNode initializeResult(String contextPath) {
@@ -98,6 +101,7 @@ public class McpSseService {
tools.add(toolDefinition(qaVectorSearchView()));
tools.add(toolDefinition(qaVectorStoreView()));
tools.add(toolDefinition(fewShotNl2SqlView()));
tools.add(toolDefinition(gameScopeView()));
result.set("tools", tools);
return result;
}
@@ -112,7 +116,7 @@ public class McpSseService {
ObjectNode properties = objectMapper.createObjectNode();
ArrayNode required = objectMapper.createArrayNode();
if (qaVectorSearchToolName().equals(toolView.name())) {
if (qaVectorSearchToolName().equals(toolView.name()) || gameScopeToolName().equals(toolView.name())) {
addStringProperty(properties, "question", "few-shot 예제 SQL을 찾을 현재 질문입니다.", 4000);
ObjectNode topK = properties.putObject("topK");
topK.put("type", "integer");
@@ -129,6 +133,10 @@ public class McpSseService {
required.add("answerSql");
} else {
addStringProperty(properties, "prompt", promptDescription(), 4000);
if (fewShotNl2SqlToolName().equals(toolView.name())) {
addStringProperty(properties, "scopeGameKey",
"선택 사항입니다. game_scope_resolve가 반환한 SUPPORTED gameKey만 전달하세요.", 128);
}
required.add("prompt");
}
schema.set("properties", properties);
@@ -152,7 +160,9 @@ public class McpSseService {
boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
boolean fewShotNl2SqlTool = fewShotNl2SqlToolName().equals(calledToolName);
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool && !fewShotNl2SqlTool) {
boolean gameScopeTool = gameScopeToolName().equals(calledToolName);
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool && !fewShotNl2SqlTool
&& !gameScopeTool) {
throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
}
@@ -173,10 +183,16 @@ public class McpSseService {
arguments.path("answerSql").asText(""),
arguments.path("answer").isMissingNode() ? null : arguments.path("answer").asText(null)
));
} else if (gameScopeTool) {
response = gameScopeResponse(gameScopeService.resolve(
token, arguments.path("question").asText("")));
} else {
String prompt = arguments.path("prompt").asText("");
response = queryTool || fewShotNl2SqlTool
response = queryTool
? selectAiService.generateAndExecute(token, prompt)
: fewShotNl2SqlTool
? selectAiService.generateAndExecute(token, prompt,
arguments.path("scopeGameKey").asText(""))
: selectAiService.generatePrompt(token, prompt);
}
} catch (VpdTokenAccessDeniedException ignored) {
@@ -231,6 +247,28 @@ public class McpSseService {
return response;
}
/** Converts DB-derived scope facts to a tool response; this tool never executes query SQL. */
private ObjectNode gameScopeResponse(GameScopeService.GameScopeResult result) {
ObjectNode response = objectMapper.createObjectNode();
response.put("status", result.status());
response.put("instruction", "반환된 SUPPORTED scope에만 Few-shot NL2SQL을 호출하세요.");
response.put("question", result.question());
ArrayNode scopes = response.putArray("scopes");
for (GameScopeService.GameScope scope : result.scopes()) {
ObjectNode item = scopes.addObject();
item.put("gameKey", scope.gameKey());
item.put("displayName", scope.displayName());
item.put("matchedAlias", scope.matchedAlias());
item.put("status", scope.status());
item.put("reasonCode", scope.reasonCode());
item.put("aliasPriority", scope.aliasPriority());
item.put("scopeVersion", scope.scopeVersion());
item.put("nextAction", "SUPPORTED".equals(scope.status())
? "CALL_FEW_SHOT_NL2SQL" : "REPORT_UNSUPPORTED");
}
return response;
}
private ObjectNode tokenAccessDeniedResult() {
ObjectNode payload = objectMapper.createObjectNode();
payload.put("status", "VPD_TOKEN_DENIED");
@@ -287,6 +325,12 @@ public class McpSseService {
fewShotNl2SqlToolLabel(), SELECT_AI_TOOL_PATH);
}
private McpToolView gameScopeView() {
return new McpToolView(
gameScopeToolName(), gameScopeToolDescription(), -1L,
gameScopeToolLabel(), SELECT_AI_TOOL_PATH);
}
private String selectAiProfile() {
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
@@ -379,6 +423,21 @@ public class McpSseService {
: mcpProperties.resolvedFewShotNl2SqlToolDescription();
}
private String gameScopeToolName() {
return mcpProperties == null ? "oracle.select_ai.game_scope_resolve"
: mcpProperties.resolvedGameScopeToolName();
}
private String gameScopeToolLabel() {
return mcpProperties == null ? "게임 조회 범위 확인" : mcpProperties.resolvedGameScopeToolLabel();
}
private String gameScopeToolDescription() {
return mcpProperties == null
? "질문에서 언급된 게임 별칭을 DB 게임 범위 view로 확인합니다."
: mcpProperties.resolvedGameScopeToolDescription();
}
private String pretty(Object value) {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);