13. 알림·구독·로깅 등 부가 기능
- 서버→클라이언트 알림(list_changed, updated, progress, log)을 구분해 이해한다
- 클라이언트 기능(sampling·elicitation·roots)이 "서버가 클라이언트에 요청"하는 역방향이라는 점을 안다
- 이 기능들이 모두 capability 협상과 human-in-the-loop에 묶인다는 원칙을 이해한다
지금까지의 방향, 그리고 역방향
지금까지 본 Tools·Resources·Prompts는 모두 클라이언트 → 서버 방향(서버가 노출하고 클라이언트가 호출)이었습니다. 이 장은 그 외의 두 묶음을 다룹니다.
flowchart LR
subgraph SERVER_FEAT["서버 기능 (10~12장)"]
T["Tools"]
R["Resources"]
P["Prompts"]
end
subgraph UTIL["유틸리티 (알림·진행·로그)"]
N["list_changed / updated"]
PR["progress"]
LOG["log message"]
end
subgraph CLIENT_FEAT["클라이언트 기능 (서버가 요청)"]
SAMP["sampling"]
ELIC["elicitation"]
ROOTS["roots"]
end
classDef sf fill:#5eead4,stroke:#0f766e,color:#000;
classDef ut fill:#bfdbfe,stroke:#1d4ed8,color:#000;
classDef cf fill:#fde68a,stroke:#b45309,color:#000;
class T,R,P sf;
class N,PR,LOG ut;
class SAMP,ELIC,ROOTS cf;
아키텍처 개요는 이렇게 정리합니다 — 클라이언트 기능은 서버가 호스트 LLM에 sampling하고, 사용자에게 입력을 elicit하고, 클라이언트에 메시지를 로깅하게 해줍니다. 유틸리티 기능은 실시간 업데이트 알림과 긴 작업의 진행 추적 같은 부가 능력을 지원합니다.
1) 알림 — 한 방향 통지 (복습 + 확장)
알림은 id가 없는 한 방향 메시지입니다(04장). MCP의 주요 알림을 모으면:
| 알림 | 방향 | 의미 |
|---|---|---|
notifications/initialized | C → S | 핸드셰이크 완료 |
notifications/tools/list_changed | S → C | 도구 목록 변경 → 클라이언트가 tools/list 재호출 |
notifications/resources/list_changed | S → C | 리소스 목록 변경 |
notifications/resources/updated | S → C | 구독한 리소스 내용 변경 |
notifications/prompts/list_changed | S → C | 프롬프트 목록 변경 |
notifications/progress | 양방향 | 긴 작업의 진행률 |
notifications/message | S → C | 로그 메시지 |
notifications/cancelled | 양방향 | 진행 중 요청 취소 |
listChanged: true를 선언한 경우에만 의미가 있습니다. 선언하지 않았으면 보내지 않습니다.2) 진행(progress)과 취소(cancellation)
긴 작업(대용량 처리, 외부 API 폴링)에서 클라이언트가 마냥 기다리지 않게, 서버는 진행 알림을 보낼 수 있습니다.
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": { "progressToken": "abc", "progress": 50, "total": 100 }
}
06장에서 본 타임아웃과 맞물립니다 — 클라이언트는 요청별 타임아웃을 두되, 진행 알림이 오면 타임아웃을 연장하고, 응답이 없으면 notifications/cancelled로 취소를 알려 서버가 처리를 멈추게 합니다(단, 최대 상한은 유지).
3) 로깅 — notifications/message
서버는 클라이언트에 구조화된 로그 메시지를 보낼 수 있습니다. stdio에서 stderr 로그(07장)와 달리, 이건 프로토콜 차원의 로깅이라 클라이언트가 레벨을 받아 표시·필터링할 수 있습니다.
print로 stdout을 오염시키지 말고, MCP 로깅(notifications/message)이나 stderr를 쓰세요. 프레임워크(FastMCP)는 보통 로깅 헬퍼를 제공합니다.4) 클라이언트 기능 — 서버가 클라이언트에 요청 (역방향)
여기가 많은 튜토리얼이 빠뜨리는 부분입니다. MCP 서버는 보통 "수동적"입니다 — 클라이언트가 도구를 부르면 일하고 결과를 돌려주죠. 하지만 세 기능이 이 관계를 뒤집습니다. 클라이언트가 서버에 노출하는 세 capability — sampling(서버가 클라이언트의 LLM에 추론을 요청), roots(클라이언트가 서버에 접근 가능한 파일 경로를 알려줌), elicitation(서버가 클라이언트를 통해 사용자에게 구조화된 입력을 요청) — 이 그것입니다.
initialize에서 해당 capability를 광고한 경우에만 서버가 쓸 수 있습니다. 클라이언트가 sampling을 광고하지 않았으면, 서버는 sampling/createMessage를 보내선 안 됩니다. 코드에서도 쓰기 전에 지원 여부를 확인합니다(if context.sampleEnabled(): ... 식).sampling — 서버가 모델에게 "생각 좀 해줘"
sampling/createMessage로, 서버가 클라이언트를 통해 호스트의 LLM에 완성을 요청합니다. 서버는 메시지 목록과 (선택적) 시스템 프롬프트·모델 선호를 보내고, 클라이언트가 (사용자 승인을 거쳐) 모델에 전달한 뒤 결과를 돌려줍니다.
sequenceDiagram
participant C as Client
participant LLM as Host LLM
participant S as Server
C->>S: tools/call (예: analyze_code)
S->>C: sampling/createMessage ("이 코드의 보안 취약점 분석")
Note over C: (사용자 승인)
C->>LLM: 서버의 프롬프트 전달
LLM-->>C: 분석 결과
C-->>S: CreateMessageResult (완성)
S-->>C: tool 결과 (LLM 분석을 활용)
이게 강력한 이유: 서버가 스스로 AI 애플리케이션이 되지 않고도, 자기 로직 안에서 LLM 추론을 빌려 쓸 수 있습니다. 게다가 API 키를 노출하지 않고 — 모델 접근은 클라이언트가 쥐고 있으니까요.
elicitation — 서버가 사용자에게 "이 정보 좀 줘"
elicitation/create로, 서버가 실행을 잠시 멈추고 클라이언트를 통해 사용자에게 구조화된 입력을 요청합니다. 서버는 사람이 읽을 메시지와 (선택적) 기대 응답 구조를 기술한 JSON 스키마(requestedSchema)를 보내고, 클라이언트가 적절한 입력 UI를 렌더링합니다. 사용자는 수락(데이터 제공)·거절·취소 중 하나로 답합니다.
쓰임새: 워크플로 중간에 "이 고객이 맞나요?" 같은 확인이나, 앞 단계 결과에 따라 필요한 값(선호·승인·누락 파라미터)을 그 시점에 모읍니다. 다단계 대화 루프 없이 구조화된 입력을 받는 깔끔한 방법입니다.
roots — 클라이언트가 서버에 "여기까지 봐도 돼"
클라이언트가 서버에 파일시스템 루트(작업 경로)를 알려줍니다. 서버는 파일 작업을 추측하지 않고 허용된 경계 안에서 수행합니다. 클라이언트는 roots로 접근 범위를 강제해, 예컨대 "이 사용자의 데이터만 접근"을 보장할 수 있습니다.
🔒 세 기능 모두 human-in-the-loop: sampling은 사용자 승인을 거쳐 모델을 호출하고, elicitation은 사용자가 직접 수락/거절하며, roots는 접근 경계를 강제합니다. 이들은 "서버가 마음대로 모델을 쓰거나 사용자 데이터를 캐가는" 것을 막는 통제 장치입니다. 그래서 클라이언트의 명시적 capability 광고와 승인이 필수입니다. sampling으로 모델을 부를 때 서버가 보내는 프롬프트, elicitation으로 받는 입력은 모두 신뢰 경계를 넘나드니 검증·표시가 필요합니다.
조합 — 똑똑한 서버
이 기능들은 잘 어울립니다. 한 서버가 elicitation으로 "어떤 파일을 분석할까요?"를 사용자에게 묻고, sampling으로 그 파일을 LLM에 분석시키는 식으로 엮을 수 있습니다. 수동적 도구 제공자를 넘어, 멈추고·묻고·추론한 뒤 도구 호출을 끝내는 서버가 됩니다.
이 장에서 배운 것
- 알림은 한 방향 통지(list_changed·updated·progress·message·cancelled). list_changed는 해당 capability 선언 시에만.
- 긴 작업은 progress 알림 + 타임아웃 연장 + cancellation으로 다룬다. 로깅은
notifications/message(stdout 오염 금지). - 클라이언트 기능(sampling·elicitation·roots) 은 서버가 클라이언트에 요청하는 역방향이며, 클라이언트의 capability 광고와 human-in-the-loop 가 전제다.
- 클라이언트 지원은 고르지 않으니 선택적 향상으로 설계하고, 미래 스펙 변경(RC 초안)은 단정하지 않는다.
✍️ 확인 문제
- 서버가 자기 로직 안에서 "이 텍스트를 요약"하려 한다. 스스로 LLM API 키를 두는 대신 어떤 클라이언트 기능을 쓰면 되나? 그 이점은?
notifications/resources/list_changed와notifications/resources/updated는 각각 언제 발생하나? 차이는?- 서버가 elicitation으로 사용자에게 질문했는데 사용자가 "취소"를 골랐다. 서버는 이를 어떻게 처리해야 할까?
4부 끝. 다음은 5부 · 서버 직접 만들기(14장 첫 서버)로 이어집니다. 드디어 직접 코드를 짭니다.