MCP의 elicitation을 사용하여 사용자 문의가 필요한 MCP 서버 구현하기 — 승인 대기 과정을 '대화'로 전환
요약
MCP(Model Context Protocol)의 elicitation 기능을 활용하여, 기존의 비동기 승인 대기 과정을 실시간 '대화' 기반 상호작용으로 전환하는 구현 방법을 설명합니다. 이 기능은 서버 측에서 클라이언트에게 직접 사용자 문의를 요청할 수 있게 하여, 도구 실행 흐름을 멈추지 않고 연속적으로 처리할 수 있도록 합니다.
핵심 포인트
- elicitation은 일반적인 client→server 호출과 달리 server→client의 역방향 요청입니다.
- 사용자 경험 개선을 위해 승인 과정을 '대화'로 전환하는 것이 핵심 목표입니다.
- 스키마는 최상위 레벨에서 string/number/boolean/enum 등 플랫한 프리미티브만 지원합니다.
- 여러 건의 처리가 필요할 경우, 한 번에 처리하려 하기보다 enum 등으로 분기하여 1 액션당 1회 문의로 루프를 설계해야 합니다.
저는 혼자서 Claude Code를 이용해 회사를 운영하고 있으며, 외부 액션(SNS 게시물 작성, 청구서 발행, 배포)은 반드시 '초안 → 승인 → 실행'의 파이프라인을 거치는 방식으로 운영하고 있습니다. 하지만 이 승인 과정은 처음에는 Markdown 큐에 쌓아두고 나중에 사람이 읽는 형태로 처리했기 때문에, 실행이 멈추는 것이 상당히 아쉬웠습니다.
MCP의 elicitation을 사용하면 도구 실행 중간에 서버 측에서 클라이언트(즉, Claude Code)를 통해 사용자에게 문의하고 답변을 받아 처리를 계속할 수 있습니다. 승인 과정이 '나중에 읽을 파일'에서 '그 자리에서의 대화'로 바뀌는 것입니다. 이 글은 그 구현 내용입니다.
MCP는 보통 client → server의 일방향 도구 호출이지만, elicitation은 그 반대 방향의 요청입니다.
Claude Code ──tools/call──▶ MCP 서버
│
◀─elicitation/create─┤ '본격적으로 배포하시겠습니까?' + JSON Schema
...
주의해야 할 중요한 제약사항이 세 가지 있습니다.
- 클라이언트가 지원하지 않으면 사용할 수 없습니다.
initialize의 응답에서capabilities.elicitation이 돌아오는지 반드시 확인해야 합니다. -
스키마는 플랫한 객체만 가능합니다. 최상위 속성은string/number/boolean/enum과 같은 프리미티브여야 합니다. 중첩된 object나 array는 사양상 지원되지 않으며, 클라이언트에 따라 조용히 무시될 수 있습니다. -
응답은 3가지 상태입니다.accept(입력 있음)/decline(명시적 거부)/cancel(다이얼로그 닫음).decline과cancel를 동일하게 취급하면 문제가 발생합니다(후술).
제가 실제로 운영하고 있는 승인 파이프라인을 MCP로 구현한 내용입니다. 기존의 셸 구현은 이런 형태이며, 승인 결과를 API에 전달하여 Slack으로 전송하고 git commit 합니다.
# .company/scripts/auto-approve.sh (발췌 및 실제 운영 코드)
result=$(api_post
printf '%s\n'
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"elicitation":{}},"clientInfo":{"name":"probe","version":"0"}}}'
'{"jsonrpc":"2.0","method":"notifications/initialized"}'
...
`elicitation/create`
**서버 측에서 리퀘스트가 흘러나오는 것**이 보이면 성공입니다 (이 수동 테스트는 응답을 반환하지 않으므로 거기서 멈춥니다). 대화까지 포함하여 시도하려면 `npx @modelcontextprotocol/inspector node dist/index.js`
하는 것이 빠릅니다.
capability를 신고하지 않는 형태(`"capabilities":{}`)로도 테스트하고, `queueDraft`
쪽에 폴백(fallback)하는 것도 반드시 확인해 주세요. 여기서 확인하지 않고 본 서비스에 투입하면, 미지원 클라이언트에서 `elicitInput()`
이 예외가 되어 도구 전체가 다운됩니다.
**중첩된 스키마는 통과하지 못합니다.** 처음에 `{ items: [{id, decision}] }`
같은 배열로 '승인 대기 5건을 한 번에 처리'하는 UI를 만들려고 했더니, 통째로 무시되었습니다. 사양이 플랫한 프리미티브(primitive)에 한정되어 있으므로, **1 액션당 1회의 문의**로 루프를 분해하는 것이 정답입니다. 건수가 많을 때는 먼저 `enum`
으로 '전부 승인 / 1건씩 보기 / 전부 거절'을 물어본 후 분기하면 처리 횟수를 줄일 수 있습니다.
**decline과 cancel을 혼용하지 마세요.** cancel(다이얼로그를 닫음)을 '거절'로 기록하면, 자리를 비웠던 것만으로도 거절 로그가 남게 됩니다. 저는 cancel을 미결 상태로 취급하고,
`isError`
도 설정하지 않고 '재시도해도 좋다'고 반환하도록 했습니다. 반대로 decline은 `isError: true`
로 설정하여 에이전트가 임의로 계속 진행하지 못하게 합니다. **타임아웃.** 사람은 응답할 때까지 서버는 기다립니다. cron에서 비인간적으로 실행되는 경로에서 이 도구를 호출하면 영원히 멈추므로, 비인간 실행에서는 capability가 없음 = 큐에 떨어진다는 설계로 조정했습니다. 자동 승인(임계값 내)은 기존의 `auto-approve.sh`
대로 유지하고, 임계값을 초과할 때만 elicitation을 하는 이층 구조입니다.
**승인 프롬프트에 전체 내용을 넣으세요.** 'SNS 게시물을 승인하시겠습니까?'만으로는 내용이 보이지 않아 CEO(즉, 저)가 결국 파일을 열어야 의미가 있습니다. 게시물 본문・금액을 메시지에 모두 포함하면, 아침 확인 작업이 정말 수십 초 만에 끝납니다.
승인 대기를 큐에 쌓아두는 운영 방식은 안전하지만 **사람이 병목 지점이 되는 곳을 코드 외부에 두는** 설계였습니다. elicitation은 그 병목 지점을 도구 내부로 되돌려줍니다. 멈추는 것은 같지만, 멈춘 순간 질문받기 때문에 재개 속도가 빠릅니다.
실제로 승인의 평균 리드 타임이 '다음 날 아침 일괄'에서 '그 자리'로 바뀌면서, 하루에 처리할 수 있는 대외 액션의 본수가 늘었습니다. 거버넌스를 느슨하게 하지 않으면서 처리량을 높일 수 있다는 것이 이 기능의 가장 경영적인 가치라고 생각합니다.
MCP 서버의 설계・인증・실제 운영까지 정리한 저희 회사의 책이 있습니다.
- 📘
**『Claude Code × MCP 서버 개발 입문』**— 도구 설계, 트랜스포트 선택, 클라이언트 대응 차이점, 실제 운영 모니터링까지
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기