LabHub
배우기 러닝패스 코스

The agent dropped my database

Who called what, and when — audit log, schema validation, timeouts

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

운영되는 MCP 서버에는 네 가지가 더 있어야 한다. 모든 호출을 남기는 감사 로그, 실행 전 인자 스키마 검증, 도구별 실행 시간 상한, 파괴적 도구의 호출 빈도 제한. 스펙은 앞의 둘과 마지막을 서버의 MUST 로, 타임아웃을 클라이언트의 SHOULD 로 적어 두었고, 실무에서는 넷 다 서버가 갖는 편이 안전하다.

Concept map: 감사 로그는 호출 단위다. · 스키마 검증은 실행 앞에 있다. · 타임아웃은 서버가 스스로 건다. · 로그를 프로토콜로도 보낸다.

왜 이게 필요했나

사고가 난 뒤 첫 질문은 "누가 언제 무엇을 어떤 인자로 불렀나" 다. 그 답이 없으면 원인을 추측으로 메우고, 추측으로 고친 서버는 같은 사고를 다시 낸다. 앞 모듈의 사고 기록에 cause= 를 적을 수 있었던 것은 우연히 요청 로그가 남아 있었기 때문이고, 실제 운영에서는 그 우연에 기대면 안 된다.

도구 스펙 의 보안 고려 절은 서버가 모든 도구 입력을 검증하고, 접근 제어를 구현하고, 호출 빈도를 제한하고, 출력을 정화해야 한다(MUST)고 적는다. 클라이언트에는 민감한 작업의 사용자 확인, 호출 전 입력 표시, 결과 검증, 도구 호출의 타임아웃, 감사용 사용 기록을 권한다(SHOULD). 이 모듈은 그 목록을 서버 쪽 코드로 옮긴다 — 클라이언트가 어떤 호스트인지 서버는 고를 수 없기 때문이다.

어떻게 동작하나

감사 로그는 호출 단위다. 도구 실행 함수 하나를 감싸 시작 시각을 재고, 결과의 isError 로 성공 여부를 정하고, 한 줄 JSON 을 파일에 덧붙인다. 남길 것은 언제(ts), 무엇을(tool), 어떤 인자로(arguments), 됐는가(ok), 얼마나 걸렸나(duration_ms), 실패했다면 왜(error)다. 프로토콜 오류로 거절된 호출도 남긴다 — 잘못된 인자를 반복해서 보내는 클라이언트는 그 자체가 신호다. 경로를 환경변수로 받게 하면 채점기든 시험이든 임시 파일로 돌려 볼 수 있다.

스키마 검증은 실행 앞에 있다. tools/list 로 낸 inputSchema 는 모델에게 준 약속이고, 그 약속을 서버가 스스로 지키는 것이 검증이다. required 가 빠졌거나 type 이 어긋나면 실행하지 않고 JSON-RPC 2.0-32602 Invalid params 로 답한다. 도구가 돌다 실패한 것이 아니라 요청이 틀린 것이므로 isError 가 아니라 프로토콜 오류다. 파이썬에서 주의할 점 하나 — boolint 의 하위 타입이라 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']}")

타임아웃은 서버가 스스로 건다. 라이프사이클 의 타임아웃 절은 보낸 요청마다 타임아웃을 두어 멈춘 연결과 자원 고갈을 막으라고 하고, 진행 알림이 와도 최대 타임아웃은 반드시 지키라고 한다. 클라이언트 쪽 이야기지만 서버도 같은 이유로 자기 도구에 상한을 건다 — 멈춘 도구 하나가 단일 스레드 서버 전체를 잡아 두기 때문이다. 표준 라이브러리의 signal.alarm 으로 N 초 뒤 SIGALRM 을 받게 하고 핸들러에서 예외를 던지면 time.sleep 이나 느린 쿼리 도중에도 빠져나온다. 끝나면 signal.alarm(0) 으로 푼다. 시간을 넘긴 호출은 isError: true 로 답하고 감사 로그에 실패로 남긴다.

로그를 프로토콜로도 보낸다. 로깅 유틸리티 는 서버가 logging 능력을 선언하고 notifications/message 알림으로 구조화된 로그를 보내는 방법을 정한다. level 은 RFC 5424 의 여덟 수준(debug·info·notice· warning·error·critical·alert·emergency)이고, loggerdata 가 붙는다. 알림 이므로 id 가 없다. stderr 로그가 사람을 위한 것이라면 이것은 호스트가 구조적으로 받아 화면에 보이거나 수집하는 로그다. 호출마다 응답 앞에 한 줄 보내면 된다.

호출 빈도 제한은 파괴적 도구부터. 세션은 프로세스 하나이므로 모듈 전역 딕셔너리로 도구별 횟수를 세면 된다. delete_order 를 세션당 3회로 묶으면, 잘못된 루프에 빠진 에이전트가 주문을 전부 지우는 일은 세 건에서 멈춘다. 넘긴 호출은 지우지 않고 isError: true 로 답한다.

현장에서 만나는 모습

감사 로그가 생기면 그것을 읽는 도구가 필요해진다. 도구별 호출 수와 실패 수를 세는 스크립트 하나면 "지난 한 시간에 delete_order 가 몇 번 불렸고 몇 번 거절 됐나" 를 답할 수 있고, 그 숫자가 곧 알림 규칙의 재료다. 실패율이 갑자기 오르면 스키마를 바꿨는데 모델 쪽 설명을 안 고친 것이고, 타임아웃이 늘면 뒤의 DB 가 느려진 것이다.

타임아웃 값은 도구마다 다르다. 조회는 2초면 충분하지만 보고서 생성은 30초가 필요할 수 있다. 하나의 값으로 시작하되 감사 로그의 duration_ms 분포를 보고 도구별로 나누는 것이 순서다 — 처음부터 도구마다 다른 값을 추측해 넣으면 근거 없는 숫자가 코드에 남는다.

다음 실습에서 할 것

앞 모듈의 서버에 감사 로그, 스키마 검증, slow_report 도구와 2초 상한, 로그 알림, delete_order 3회 제한을 차례로 넣고, 마지막에 감사 로그를 읽어 도구별 호출·실패 수를 요약하는 스크립트를 쓴다.