ux #463: streamline MCP reasoning flow

This commit is contained in:
devmrko
2026-06-25 22:10:21 +09:00
parent 8fddf1033e
commit 1f924f67de
5 changed files with 152 additions and 18 deletions

View File

@@ -0,0 +1,83 @@
# 설계서: MCP Reasoning/ORDS 검증 탭 사용 흐름 정리 (#463)
> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-25
> **추적성** — Redmine: #463 · 관련 ADR: 없음
> · 구현 파일: `mcp-reasoning.html`, `mcp-reasoning-result.html`, `probe.html`, `McpReasoningService` · 테스트: `mvn test`, Spring Boot 기동
## 1. 목적 (Why)
운영자가 보호 객체와 Bearer Token을 선택한 뒤 ORDS 검증과 MCP Reasoning을 자연스럽게 이어서 실행하고, 모델 답변을 표/요약 중심으로 읽을 수 있게 한다.
## 2. 범위 (Scope)
- **포함**: MCP Reasoning 기본 질문/원클릭 질문 개선, 결과 Markdown 렌더링, 실행 증거 요약, ORDS 검증에서 Reasoning 이동 안내.
- **제외**: MCP protocol 자체 변경, LLM 모델 변경, token 자동 공유.
## 3. 인수조건 (Acceptance Criteria)
- [ ] Reasoning 답변이 Markdown view로 렌더링되어 표가 표시된다.
- [ ] Prompt가 요약 먼저, 상세 나중 구조를 요구한다.
- [ ] 질문 없이 실행해도 권한 검증에 맞는 기본 질문이 사용된다.
- [ ] ORDS 검증 화면에서 같은 흐름의 다음 단계로 Reasoning 탭을 인지할 수 있다.
- [ ] `mvn test`와 Spring Boot 기동이 통과한다.
## 4. 컨텍스트 & 제약
- token 원문은 화면 이동으로 자동 전달하지 않는다.
- Reasoning은 실제 ORDS 호출 결과를 evidence JSON으로 모델에 전달한다.
- Markdown 렌더러는 기존 `data-markdown-view`를 사용한다.
## 5. 아키텍처 개요
```
ORDS 검증
-> 결과 확인
-> MCP Reasoning 이동
-> 보호 객체 선택
-> token 입력
-> 기본 질문 또는 원클릭 질문
-> ORDS tool 실행
-> Markdown answer + evidence 표시
```
## 6. 데이터 모델
- `McpReasoningResult.answer`: Markdown text.
- `ProbeResult`: request/response/evidence table의 근거.
## 7. 함수 명세 (Function Specs)
| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? |
|------|-----------|----------------|------|------|-----------|-------|
| `buildPrompt` | 모델에 전달할 업무형 권한 분석 prompt 생성 | Java method | question, tool, evidence | prompt | 없음 | 단순 |
| `fallbackAnswer` | AI 미설정 시 evidence 기반 기본 설명 생성 | Java method | question, probeResult | Markdown text | 없음 | 단순 |
## 8. 흐름 / 알고리즘
1. 질문이 비어 있으면 기본 권한 검증 질문을 사용한다.
2. ORDS probe를 실행한다.
3. evidence JSON을 구성한다.
4. 모델 답변은 요약, 판단 근거 표, 상세, 다음 조치 순서로 요청한다.
5. 화면은 answer를 Markdown으로 렌더링한다.
## 9. 엣지케이스 & 에러 처리
- AI 미설정: Markdown fallback으로 ORDS 상태와 row count를 보여준다.
- ORDS 실패: 모델 호출 여부와 무관하게 request/response evidence를 표시한다.
- token 원문은 prompt/evidence에 포함하지 않는다.
## 10. 테스트 계획
- `mvn test`
- Spring Boot 재기동
- `/mcp-reasoning` 인증 요청 HTTP 200 확인
## 11. 리스크 & 대안 검토
- 선택: 화면/프롬프트 개선. 데이터 모델 변경 없이 사용 흐름을 개선한다.
- 대안: wizard UI. 구현 범위가 커지고 기존 탭 구조를 크게 바꿔야 한다.
## 12. 미해결 질문 (Open Questions)
- token 선택 드롭다운과 token 원문 자동 복호화는 보안 정책상 별도 검토가 필요하다.

View File

@@ -114,7 +114,7 @@ public class McpReasoningService {
private String buildPrompt(String question, McpToolView tool, String evidenceJson) { private String buildPrompt(String question, McpToolView tool, String evidenceJson) {
String normalizedQuestion = question == null || question.isBlank() String normalizedQuestion = question == null || question.isBlank()
? "이 ORDS/VPD 검증 결과를 권한 관점에서 요약해줘." ? "요약부터 작성해줘. 이 ORDS/VPD 검증 결과에서 조회 행 수, 주요 식별자, NULL 처리 여부, 권한 범위, 다음 확인 조치를 정리해줘."
: question.trim(); : question.trim();
return """ return """
질문: 질문:
@@ -131,8 +131,11 @@ public class McpReasoningService {
답변 요구사항: 답변 요구사항:
- 한국어로 답변한다. - 한국어로 답변한다.
- 증거 JSON에 없는 데이터는 추측하지 않는다. - 증거 JSON에 없는 데이터는 추측하지 않는다.
- 첫 섹션은 "## 요약"으로 시작하고 3줄 이내로 쓴다.
- 그 다음 "## 판단 근거" 섹션에 표를 사용해 rowCount, maskedColumns, status, errorCode를 정리한다.
- 그 다음 "## 상세" 섹션에서 반환 행과 권한 범위를 설명한다.
- 마지막 "## 다음 조치" 섹션은 운영자가 확인할 항목만 짧게 쓴다.
- VPD 행 필터, 컬럼 NULL 처리, ORDS 오류 여부를 구분한다. - VPD 행 필터, 컬럼 NULL 처리, ORDS 오류 여부를 구분한다.
- 운영자가 다음에 확인할 액션이 있으면 짧게 제시한다.
""".formatted(normalizedQuestion, tool.name(), tool.displayName(), tool.ordsPath(), evidenceJson); """.formatted(normalizedQuestion, tool.name(), tool.displayName(), tool.ordsPath(), evidenceJson);
} }
@@ -145,12 +148,53 @@ public class McpReasoningService {
private String fallbackAnswer(String question, ProbeResult result) { private String fallbackAnswer(String question, ProbeResult result) {
if (result.status() == ProbeStatus.SUCCESS) { if (result.status() == ProbeStatus.SUCCESS) {
return "AI base URL/API Key 설정이 없어 모델 호출은 건너뛰었습니다. ORDS 호출은 성공했고 " return """
+ result.rowCount() + "건이 반환되었습니다. 질문: " + safeQuestion(question); ## 요약
AI base URL/API Key 설정이 없어 모델 호출은 건너뛰었습니다.
ORDS 호출은 성공했고 %d건이 반환되었습니다.
## 판단 근거
| 항목 | 값 |
| --- | --- |
| status | %s |
| rowCount | %d |
| maskedColumns | %s |
## 상세
질문: %s
## 다음 조치
- 모델 설명이 필요하면 AI 설정을 확인하세요.
- 행/컬럼 결과는 아래 Tool Evidence JSON과 Response Body를 기준으로 확인하세요.
""".formatted(
result.rowCount(),
result.status().name(),
result.rowCount(),
result.maskedColumns().isEmpty() ? "-" : String.join(", ", result.maskedColumns()),
safeQuestion(question)
);
} }
return "AI base URL/API Key 설정이 없어 모델 호출은 건너뛰었습니다. ORDS 도구 실행 상태는 " return """
+ result.status().name() + "입니다. 오류: " ## 요약
+ (result.errorMessage() == null ? "없음" : result.errorMessage()); AI base URL/API Key 설정이 없어 모델 호출은 건너뛰었습니다.
ORDS 도구 실행은 성공 상태가 아닙니다.
## 판단 근거
| 항목 | 값 |
| --- | --- |
| status | %s |
| errorMessage | %s |
## 상세
request/response 증거를 확인해 ORDS path, token, handler 오류를 구분하세요.
## 다음 조치
- ORDS path와 token 만료/폐기 상태를 확인하세요.
- handler 오류면 Response Body의 ORDS error를 확인하세요.
""".formatted(
result.status().name(),
result.errorMessage() == null ? "없음" : result.errorMessage()
);
} }
private String safeQuestion(String question) { private String safeQuestion(String question) {

View File

@@ -13,11 +13,14 @@
<span>Tool: <code th:text="${result.toolName()}">tool</code></span> <span>Tool: <code th:text="${result.toolName()}">tool</code></span>
<span>ORDS Status: <strong th:text="${result.probeResult().status()}">SUCCESS</strong></span> <span>ORDS Status: <strong th:text="${result.probeResult().status()}">SUCCESS</strong></span>
<span>Rows: <strong th:text="${result.probeResult().rowCount()}">0</strong></span> <span>Rows: <strong th:text="${result.probeResult().rowCount()}">0</strong></span>
<span th:if="${!#lists.isEmpty(result.probeResult().maskedColumns())}">
Masked: <code th:text="${#strings.listJoin(result.probeResult().maskedColumns(), ', ')}"></code>
</span>
</div> </div>
<section class="ai-answer"> <section class="ai-answer">
<h3>Answer</h3> <h3>요약 및 판단</h3>
<pre th:text="${result.answer()}">answer</pre> <div class="markdown-view" data-markdown-view th:text="${result.answer()}">answer</div>
</section> </section>
<div class="probe-exchange-grid"> <div class="probe-exchange-grid">

View File

@@ -43,23 +43,23 @@
<label class="span-2"> <label class="span-2">
질문 질문
<textarea class="form-control" id="mcp-reasoning-question" name="question" rows="3" <textarea class="form-control" id="mcp-reasoning-question" name="question" rows="3"
placeholder="비워도 기본 질문으로 실행됩니다."></textarea> placeholder="비워도 조회 행, NULL 처리, 권한 범위, 다음 조치를 요약합니다."></textarea>
</label> </label>
<div class="question-presets span-2" aria-label="질문 예시"> <div class="question-presets span-2" aria-label="질문 예시">
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="이 토큰으로 조회 가능한 행 수 주요 값을 먼저 요약하고, 어떤 VPD 조건이 적용된 결과인지 추정해줘."> data-question="요약부터 작성해줘. 이 토큰으로 조회 가능한 행 수, 주요 식별자, NULL 처리 여부, 권한 범위를 표로 정리하고 상세 근거를 이어서 설명해줘.">
조회 결과 요약 기본 분석
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="반환된 행에서 NULL 처리된 컬럼이 있는지 확인하고, 민감 컬럼 권한 관점에서 설명해줘."> data-question="반환된 컬럼과 maskedColumns를 기준으로 민감 컬럼이 NULL 처리 또는 미노출됐는지 먼저 요약하고, role/permission 설정에서 확인할 항목을 정리해줘.">
NULL 처리 확인 민감 컬럼 점검
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="이 토큰이 전체 조회 권한인지, 부서/본인/조건 제한 권한인지 결과 증거를 기준으로 판단해줘."> data-question="반환된 행 수와 행의 부서/사번 값을 근거로 이 토큰이 전체 조회, 부서 제한, 본인 제한, 조건 제한 중 어디에 가까운지 판단해줘. 추정이면 추정이라고 표시해줘.">
권한 범위 판단 권한 범위 판단
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="운영자가 확인해야 할 이상 징후나 설정 불일치 가능성을 짧게 정리해줘."> data-question="운영자가 확인해야 할 이상 징후를 먼저 bullet로 요약하고, ORDS path, VPD policy, permission rule, token 상태 중 어디를 봐야 하는지 제시해줘.">
운영 점검 요약 운영 점검 요약
</button> </button>
</div> </div>
@@ -87,7 +87,7 @@
<td> <td>
<button class="btn btn-sm rw-btn-secondary reasoning-object-preset" <button class="btn btn-sm rw-btn-secondary reasoning-object-preset"
type="button" type="button"
th:attr="data-object-id=${tool.objectId()},data-question=${tool.displayName() + '에서 이 토큰으로 조회 가능한 행 NULL 처리 컬럼을 요약해줘.'}"> th:attr="data-object-id=${tool.objectId()},data-question=${tool.displayName() + '에서 이 토큰으로 조회 가능한 행, NULL 처리 컬럼, 권한 범위를 요약 먼저 표로 정리해줘.'}">
선택 선택
</button> </button>
</td> </td>
@@ -104,7 +104,7 @@
</section> </section>
<section id="mcp-result" class="content-band"> <section id="mcp-result" class="content-band">
<div class="text-muted">Reasoning 결과가 여기에 표시됩니다.</div> <div class="text-muted">실행하면 ORDS 호출 결과, 모델 요약, request/response 증거가 한 번에 표시됩니다.</div>
</section> </section>
</main> </main>
<script> <script>

View File

@@ -10,6 +10,10 @@
</div> </div>
<section class="content-band"> <section class="content-band">
<div class="section-heading">
<h2>ORDS 호출</h2>
<a class="btn btn-sm rw-btn-secondary" href="/mcp-reasoning">MCP Reasoning으로 해석</a>
</div>
<form hx-post="/probe" hx-target="#probe-result" hx-swap="innerHTML" class="form-grid"> <form hx-post="/probe" hx-target="#probe-result" hx-swap="innerHTML" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label> <label>