에이전트가 내 DB 를 지웠다 · 누가 언제 무엇을 불렀나 · 이론
누가 언제 무엇을 불렀나 — 감사 로그·스키마 검증·타임아웃
한 줄 요약
운영되는 MCP 서버에는 네 가지가 더 있어야 한다. 모든 호출을 남기는 감사 로그,
실행 전 인자 스키마 검증, 도구별 실행 시간 상한, 파괴적 도구의 호출 빈도 제한.
스펙은 앞의 둘과 마지막을 서버의 MUST 로, 타임아웃을 클라이언트의 SHOULD 로
적어 두었고, 실무에서는 넷 다 서버가 갖는 편이 안전하다.
왜 이게 필요했나
사고가 난 뒤 첫 질문은 "누가 언제 무엇을 어떤 인자로 불렀나" 다. 그 답이 없으면
원인을 추측으로 메우고, 추측으로 고친 서버는 같은 사고를 다시 낸다. 앞 모듈의
사고 기록에 cause= 를 적을 수 있었던 것은 우연히 요청 로그가 남아 있었기
때문이고, 실제 운영에서는 그 우연에 기대면 안 된다.
[도구 스펙](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
의 보안 고려 절은 서버가 모든 도구 입력을 검증하고, 접근 제어를 구현하고,
호출 빈도를 제한하고, 출력을 정화해야 한다(MUST)고 적는다. 클라이언트에는
민감한 작업의 사용자 확인, 호출 전 입력 표시, 결과 검증, **도구 호출의
타임아웃**, 감사용 사용 기록을 권한다(SHOULD). 이 모듈은 그 목록을 서버 쪽
코드로 옮긴다 — 클라이언트가 어떤 호스트인지 서버는 고를 수 없기 때문이다.
어떻게 동작하나
감사 로그는 호출 단위다. 도구 실행 함수 하나를 감싸 시작 시각을 재고, 결과의isError 로 성공 여부를 정하고, 한 줄 JSON 을 파일에 덧붙인다. 남길 것은
언제(ts), 무엇을(tool), 어떤 인자로(arguments), 됐는가(ok), 얼마나
걸렸나(duration_ms), 실패했다면 왜(error)다. 프로토콜 오류로 거절된
호출도 남긴다 — 잘못된 인자를 반복해서 보내는 클라이언트는 그 자체가 신호다.
경로를 환경변수로 받게 하면 채점기든 시험이든 임시 파일로 돌려 볼 수 있다.
스키마 검증은 실행 앞에 있다. tools/list 로 낸 inputSchema 는 모델에게
준 약속이고, 그 약속을 서버가 스스로 지키는 것이 검증이다. required 가
빠졌거나 type 이 어긋나면 실행하지 않고 [JSON-RPC 2.0](https://www.jsonrpc.org/specification)
의 -32602 Invalid params 로 답한다. 도구가 돌다 실패한 것이 아니라 요청이
틀린 것이므로 isError 가 아니라 프로토콜 오류다. 파이썬에서 주의할 점 하나 —bool 은 int 의 하위 타입이라 isinstance(True, int) 가 참이다. integer 자리에true 가 들어오는 것은 따로 막아야 한다.
TYPES = {"string": str, "integer": int, "boolean": bool}for key in schema.get("required", []): if key not in args: raise RpcError(-32602, f"Invalid params: missing {key}")for key, spec in schema["properties"].items(): if key in args and not isinstance(args[key], TYPES[spec["type"]]): raise RpcError(-32602, f"Invalid params: {key} must be {spec['type']}")타임아웃은 서버가 스스로 건다. [라이프사이클](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle)
의 타임아웃 절은 보낸 요청마다 타임아웃을 두어 멈춘 연결과 자원 고갈을 막으라고
하고, 진행 알림이 와도 최대 타임아웃은 반드시 지키라고 한다. 클라이언트 쪽
이야기지만 서버도 같은 이유로 자기 도구에 상한을 건다 — 멈춘 도구 하나가
단일 스레드 서버 전체를 잡아 두기 때문이다. 표준 라이브러리의 signal.alarm
으로 N 초 뒤 SIGALRM 을 받게 하고 핸들러에서 예외를 던지면 time.sleep 이나
느린 쿼리 도중에도 빠져나온다. 끝나면 signal.alarm(0) 으로 푼다. 시간을
넘긴 호출은 isError: true 로 답하고 감사 로그에 실패로 남긴다.
로그를 프로토콜로도 보낸다. [로깅 유틸리티](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging)
는 서버가 logging 능력을 선언하고 notifications/message 알림으로 구조화된
로그를 보내는 방법을 정한다. level 은 RFC 5424 의 여덟 수준(debug·info·notice·
warning·error·critical·alert·emergency)이고, logger 와 data 가 붙는다. 알림
이므로 id 가 없다. stderr 로그가 사람을 위한 것이라면 이것은 호스트가 구조적으로
받아 화면에 보이거나 수집하는 로그다. 호출마다 응답 앞에 한 줄 보내면 된다.
호출 빈도 제한은 파괴적 도구부터. 세션은 프로세스 하나이므로 모듈 전역
딕셔너리로 도구별 횟수를 세면 된다. delete_order 를 세션당 3회로 묶으면,
잘못된 루프에 빠진 에이전트가 주문을 전부 지우는 일은 세 건에서 멈춘다.
넘긴 호출은 지우지 않고 isError: true 로 답한다.
현장에서 만나는 모습
감사 로그가 생기면 그것을 읽는 도구가 필요해진다. 도구별 호출 수와 실패 수를
세는 스크립트 하나면 "지난 한 시간에 delete_order 가 몇 번 불렸고 몇 번 거절
됐나" 를 답할 수 있고, 그 숫자가 곧 알림 규칙의 재료다. 실패율이 갑자기 오르면
스키마를 바꿨는데 모델 쪽 설명을 안 고친 것이고, 타임아웃이 늘면 뒤의 DB 가
느려진 것이다.
타임아웃 값은 도구마다 다르다. 조회는 2초면 충분하지만 보고서 생성은 30초가
필요할 수 있다. 하나의 값으로 시작하되 감사 로그의 duration_ms 분포를 보고
도구별로 나누는 것이 순서다 — 처음부터 도구마다 다른 값을 추측해 넣으면
근거 없는 숫자가 코드에 남는다.
다음 실습에서 할 것
앞 모듈의 서버에 감사 로그, 스키마 검증, slow_report 도구와 2초 상한,
로그 알림, delete_order 3회 제한을 차례로 넣고, 마지막에 감사 로그를 읽어
도구별 호출·실패 수를 요약하는 스크립트를 쓴다.