MCP 도구의 응답은 돌아오는데 요청 로그만 비어 있다면, 로그를 요청한 조건과 수신 경로부터 확인한다. 서버가 실행한 일, 프로토콜로 전송한 로그, 화면이 보여 준 로그는 각각 관측해야 한다. 빈 로그 창만으로 호출 실패를 판정하면 정상 요청을 불필요하게 반복할 수 있다.
먼저 적용 범위를 확인하자. 2026년 10월 10일 KST에 확인한 MCP revision 2026-07-28의 기존 notifications/message 구현을 진단하는 글이다. 공식 Logging 문서는 이 revision부터 해당 기능을 deprecated로 분류하며 신규 구현에는 채택하지 않도록 권고한다. 기존 구현에는 stdio의 stderr 또는 구조화된 관측을 위한 OpenTelemetry로 이행하도록 권고한다. deprecated는 이 revision에서 이미 제거됐다는 뜻은 아니다.
아래의 메시지와 분기표는 문서 기반 설명과 합성 예시다. 실제 서버·SDK·프록시에서 장애를 재현하거나 수정한 기록은 없다. 새 관측 체계를 만들고 있다면 이 글의 레거시 메시지를 추가하기 전에 이행 방향부터 정하는 편이 맞다.
1. 예전 설정 호출과 현재 요청 메타데이터를 섞지 않는다
2025-11-25 Logging에는 최소 로그 레벨을 정하는 logging/setLevel 요청이 있다. 반면 이 글이 다루는 2026-07-28에서는 로그가 필요한 요청의 params._meta에 io.modelcontextprotocol/logLevel을 넣는다. 예전에 한 번 설정했다는 기록을 새 요청의 설정 근거로 쓰지 않는다.
2026-07-28의 요청 모델은 이전 연결이나 요청에 기대어 버전·capability 같은 문맥을 이어받지 않는다. 진단 기록에는 실제 요청의 protocolVersion과 사용 중인 클라이언트·서버 SDK 버전을 함께 적는다. SDK 이름이나 연결 표시만으로 메시지 revision을 추정하지 않는 편이 안전하다.
서버의 지원 정보는 server/discover로 확인할 수 있다. 로그 알림을 내보내는 서버는 logging capability를 선언해야 한다. 이 조회가 클라이언트의 필수 초기화라는 뜻은 아니다. 지원 선언이 없고 서버가 이미 다른 관측 경로로 이행했다면, logLevel만 추가해서 예전 로그가 생기기를 기다리지 않는다.
| 관측한 상태 | 확인할 근거 | 다음 점검 |
|---|---|---|
| 2025 예제만 따라 설정했다 | 실제 protocolVersion | 설정 호출과 요청별 메타데이터 구분 |
| 요청 로그가 하나도 없다 | 해당 요청의 logLevel | 필드 누락·레벨·서버 송신 여부 확인 |
| info만 없고 warning은 보인다 | 요청 레벨과 수신 레벨 | 필터가 의도대로 동작하는지 비교 |
| 구독 스트림만 계속 읽고 있다 | 원래 POST의 응답 | 요청 로그 수신 경로 확인 |
| 수신 기록에는 있는데 UI에 없다 | 클라이언트 필터와 표시 경로 | 서버 재호출 전에 화면 처리 점검 |
이 표는 원인을 확정하는 목록이 아니라 다음에 볼 위치를 고르는 분기다. 서버에 로그 생성 지점이 있는지, 그 지점이 실제 요청에서 실행됐는지는 구현을 읽거나 허용된 테스트 환경에서 따로 확인해야 한다.
2. logLevel은 전송을 허용하는 조건이지 수신 보장이 아니다
요청별 로그 규칙에 따르면 logLevel이 없는 요청에 서버는 notifications/message를 보내면 안 된다. 필드가 있으면 지정한 레벨 이상을 보낼 수 있다. 여기서 전송은 MAY다. 필드를 넣었다는 사실만으로 로그가 반드시 한 건 이상 생긴다고 약속할 수는 없다.
다음은 가상 서비스 상태를 읽는 get_demo_status 도구의 요청 본문이다. 실제 등록된 도구가 아니며 그대로 전송하는 실행 명령도 아니다. 예시 도구에는 추가 HTTP 매핑을 요구하는 x-mcp-header 입력이 없다고 가정한다.
{
"jsonrpc": "2.0",
"id": "log-check-201",
"method": "tools/call",
"params": {
"name": "get_demo_status",
"arguments": {
"service": "sample"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "logging-diagnostic-example",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/logLevel": "info"
}
}
}
요청 메타데이터 규칙에서 protocolVersion과 clientCapabilities는 필수이고 clientInfo는 권장이다. 위의 빈 clientCapabilities는 이 예시에서 선택적 클라이언트 기능을 선언하지 않았다는 뜻이다. logLevel을 도구의 arguments나 최상위 객체에 두지 않았는지 확인한다.
HTTP라면 본문 외에 다음 헤더도 대조한다. 실제 요청은 서버가 요구하는 인증과 나머지 HTTP 전송 요건을 갖춰야 한다. 아래는 본문과 맞춰 볼 헤더 발췌이며, 인증값이나 완전한 네트워크 요청을 담지 않았다. HTTP 요청 헤더 규칙
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_demo_status
정의된 레벨은 debug → info → notice → warning → error → critical → alert → emergency 순서다. warning을 요청했다면 info가 안 오는 것은 필터와 일치한다. warn 같은 별칭을 임의로 넣지 않는다. 알 수 없는 logLevel에 대한 -32602 거부는 규격의 SHOULD이며, 모든 구현이 반드시 같은 오류를 준다고 단정하지 않는다. LoggingLevel 스키마 · 로그 오류 처리
3. HTTP에서는 원래 요청의 응답을 끝까지 읽는다
2026-07-28 Streamable HTTP 서버는 요청마다 단일 JSON 응답 또는 SSE 응답 스트림을 선택한다. 요청 로그를 보낼 때는 원래 요청의 SSE 응답에서 최종 응답 전에 전달한다. subscriptions/listen은 리소스·목록 변경 구독 경로이므로 거기에서 요청 로그를 기다리지 않는다. HTTP 수신 규칙
예시 요청에서 서버가 info 로그를 보내기로 했다면, 다음과 같은 메시지 객체를 받을 수 있다. SSE의 data: 안에 실리는 JSON 객체만 펼쳐 놓았다. 실제 SSE 이벤트 프레이밍 전체는 아니다.
{
"jsonrpc": "2.0",
"method": "notifications/message",
"params": {
"level": "info",
"logger": "demo-status",
"data": {
"stage": "input_validated",
"sample": true
}
}
}
LoggingMessageNotificationParams에서 level과 data는 필수, logger는 선택이다. JSON-RPC 알림에는 요청용 id를 넣지 않는다. HTTP에서는 해당 POST 응답 스트림을 통해 로그가 속한 요청을 추적한다. 로그를 모으는 중간 계층에서 스트림 정보를 버리면 동시에 진행한 호출의 메시지가 한 화면에서 섞일 수 있다.
수신 코드가 최종 JSON-RPC 응답만 반환하도록 만들어졌다면 중간 알림을 버리는지 확인한다. 반대로 단일 JSON 응답을 받았다는 사실만으로 오류라고 처리해서는 안 된다. 두 응답 형식 모두 지원해야 하며, 로그 전송 자체는 선택적이다. 원본 수신에는 메시지가 있었는데 UI에서 빠졌다는 증거를 먼저 확보해야 클라이언트 표시 경로를 고칠 근거가 생긴다.
요청이 끝났는지는 대응하는 JSON-RPC 응답과 도구 결과로 판단한다. 로그의 level이 error인 사실을 곧바로 최상위 JSON-RPC error나 isError 값으로 바꾸지 않는다. 세부 판정은 isError와 JSON-RPC error 구분에 이어서 볼 수 있다. 진행률만 빠진 경우에는 progressToken 진단으로 관측 대상을 좁힌다.
4. stdio 로그를 같은 방식으로 찾으면 경로를 놓친다
stdio에서는 모든 서버 메시지가 stdout 한 통로를 공유한다. HTTP처럼 요청마다 분리된 물리적 응답 스트림이 없다. 따라서 앞 절의 “원래 POST 응답” 설명을 stdio에 그대로 적용하지 않는다. 이 글의 JSON 예시를 stdio 메시지로 다룰 때도 한 메시지를 한 줄로 직렬화해야 한다.
일반 진단 로그를 stderr로 내보내는 경우에는 호스트가 그것을 어디에서 수집·표시하는지 확인한다. stdio 전송 규격은 클라이언트가 stderr를 기록하거나 전달하거나 무시할 수 있다고 설명한다. stderr에 기록했다는 사실만으로 사용자 화면에도 나타난다고 기대할 수 없다. stdout에 일반 문장을 끼워 넣는 문제는 stdio JSON 파싱 오류 진단의 범위다.
새 관측 경로를 설계한다면 로그 메시지를 무작정 늘리기보다, 어느 요청의 어느 단계가 보이지 않는지부터 정한다. 실행 구간과 문맥 전파를 다루는 Trace·Span 진단 글이 다음 단계다. OpenTelemetry를 붙였다는 사실만으로 수집·전송·표시가 검증되는 것은 아니다.
확인 순서와 남길 기록
먼저 안전한 읽기 전용 테스트 요청 하나를 고른다. 같은 revision에서 logLevel 생략, info, warning 세 조건을 비교한다. 테스트용 서버가 합성 로그를 만들고 요청 조건에 맞춰 송신하도록 통제한 환경이어야 한다. 생략 조건에서는 송신 금지를, 설정 조건에서는 그 구현의 필터·송신·수신을 확인한다. 이는 MAY를 의무 전송으로 바꾸는 시험이 아니다. 로그가 없는 일반 요청을 반복하면서 “언젠가 한 줄은 나와야 한다”고 시험하지 않는다.
그다음 서버의 송신 여부, HTTP 원본 수신, 클라이언트 분류, UI 표시 순서로 확인한다. 한 단계에서 관측한 사실을 다음 단계의 성공으로 대신하지 않는다. 아래는 아직 수행하지 않은 통합 시험의 기록 틀이다. 요청 원문 전체를 저장할 필요는 없다.
프로토콜 revision / SDK 버전 / 전송 방식:
요청 ID / 요청한 logLevel / 서버 logging 지원:
테스트에서 생성하도록 정한 레벨:
서버 송신 관측 / 클라이언트 수신 관측:
UI 필터 / 실제 표시 여부:
최종 응답 상태 / 도구 결과:
판정: 필드 누락 / 레벨 필터 / 전송·표시 경로 / 미확정
기록에는 인증값·비밀정보·개인 식별정보를 넣지 않는다. 공격에 도움이 될 내부 시스템 상세도 제외한다. 디버그 모드로 바꿀 때는 메시지 양뿐 아니라 data에 실리는 내용도 점검한다. Logging 보안 규칙에 맞춰, 예시처럼 진단에 필요한 단계명과 합성 표식 정도로 시작할 수 있다.
검증 한계: 이 글에서 확인한 것은 공식 문서와 합성 JSON의 로컬 파싱·필드 대응이다. 실제 MCP 통신, 특정 SDK의 로그 콜백, 프록시 통과, 호스트 UI와 이행 결과는 시험하지 않았다. 장애 기록에는 “로그가 없다”에 더해 어느 revision의 어느 요청에서 어떤 레벨을 요구했고, 메시지를 마지막으로 확인한 경계가 어디인지를 남긴다. 그 근거가 있어야 로그 설정을 고칠지, 수신 코드를 고칠지, 이미 바뀐 관측 경로를 찾아야 할지 구분할 수 있다.