왜 당신의 MCP 서버는 자체 로깅이 필요할까 — 클로드 데스크톱만으로는 안 된다
I’m happy to translate the article for you, but I need the full text of the post (the content you’d like translated). Could you please paste the article’s body here? Once I have that, I’ll provide a Korean translation while keeping the source link and all formatting intact.
AI 에이전트의 보이지 않는 문제
이전 글들에서 저는 Hybrid MCP Agent를 구축했습니다. 이 에이전트는 클라우드 API(Gmail, Salesforce, Google Calendar)와 로컬 파일 시스템 작업(폴더 스캔, 파일 이동, 보고서 생성)을 모두 제어합니다. 아키텍처는 정상적으로 동작했지만, 에이전트가 실제로 무엇을 하고 있는지 전혀 파악할 수 없었습니다.
문제가 발생했을 때, 어디를 살펴봐야 할지 전혀 알 수 없었습니다.
이것이 AI‑에이전트 시스템에서의 가시성(observability) 격차입니다. Datadog이나 New Relic과 같은 전통적인 애플리케이션 모니터링 도구는 MCP 툴‑콜 체인을 위해 설계되지 않았습니다. 그리고 에이전트가 완전히 다른 두 환경—예를 들어 GCP VM과 Windows 데스크톱—에서 동시에 동작할 때, 문제는 더욱 복잡해집니다.
제가 해결한 방법은 다음과 같습니다.
아키텍처: 두 개의 데이터 스트림, 하나의 데이터베이스
원격 MCP 서버 (GCP VM)
Gmail, Salesforce, Calendar를 클라우드 API를 통해 처리합니다.
파이프라인
파트 1: FastMCP 미들웨어를 이용한 원격 로깅
서버 측에서 로그를 남겨야 하는 다섯 가지 이유:
-
데스크톱이 항상 클라이언트는 아니다.
MCP 서버는 API 통합, 다른 에이전트, 예약 작업, 혹은 동일 엔드포인트를 호출하는 다수의 사용자 등 어떤 클라이언트에서도 호출될 수 있습니다. 데스크톱 로그는 당신의 Claude Desktop 세션이 수행한 작업만 캡처합니다. 미들웨어는 호출자가 누구이든 서버에 도달하는 모든 요청을 기록합니다. -
다중 사용자 가시성을 위해서는 중앙 로그가 필요하다.
실제 운영 환경에서는 하나의 MCP 서버가 관리자, 영업, 재무 등 다양한 역할을 가진 여러 사용자를 서비스합니다 — 각각/mcp?user_id=role로 라우팅됩니다. 각 사용자의 Claude Desktop은 자신의 세션만 로그에 남깁니다. 서버‑사이드 로그가 없으면 “오후 2시에 Salesforce API 제한을 초과한 사용자는 누구인가?” 라는 질문에 답하려면 모든 노트북의 로그를 수집해야 합니다. 미들웨어는user_id로 태깅된 모든 사용자의 활동을 한 곳에 기록해, 사용자 간 분석을 손쉽게 합니다. -
엔터프라이즈 운영은 통합된 뷰를 요구한다.
이를 팀이나 부서 규모로 확대하면, 10명, 50명 혹은 그 이상의 사용자가 동일 MCP 서버를 통해 이메일을 보내고, CRM 레코드를 업데이트하고, 문서를 생성합니다. 운영 관리자는 다음과 같은 질문에 답해야 합니다:- “오늘 전체 API 사용량은 얼마인가?”
- “어떤 도구가 가장 자주 실패하는가?”
- “특정 사용자가 비정상적으로 높은 호출량을 보이고 있는가?”
모든 머신에서 데스크톱 로그를 수집하는 방식은 규모에 맞지 않습니다. 서버‑사이드 미들웨어가 통합 대시보드에 데이터를 공급함으로써, 이러한 요구를 첫날부터 충족할 수 있습니다.
-
지연 시간 정확도.
데스크톱 로그는 클라이언트가 요청을 보낸 시점과 응답을 받은 시점을 기록하므로, 전체 지속 시간에 네트워크 왕복 시간이 포함됩니다. 서버‑사이드 미들웨어는 실제 도구 실행 시간만을 측정합니다 — 느린 호출이 네트워크 문제인지 도구 성능 문제인지를 진단할 때 차이가 크게 작용합니다. -
운영 독립성.
실제 운영에서는 모니터링이 개발자의 노트북이 온라인 상태이며 업로드 스크립트를 실행하고 있다는 전제에 의존해서는 안 됩니다. 미들웨어는 실시간으로 데이터베이스에 직접 기록하므로 외부 구성 요소에 대한 의존성이 전혀 없습니다. 로컬 업로더가 충돌하거나 오프라인이 되거나 주기를 놓치더라도 — 원격 로그는 여전히 완전합니다.
요약: 데스크톱 로그 파싱은 여러분이 제어할 수 없는 환경(예: Desktop Commander 같은 npm 패키지)에서의 우회책에 불과합니다. 서버‑사이드 미들웨어는 여러분이 제어하는 환경에 적합한 정식 계측 방법입니다.
클라우드‑사이드 MCP 서버에서는 FastMCP의 미들웨어 시스템을 사용해 모든 도구 호출을 자동으로 가로챘습니다. 비즈니스 로직을 변경할 필요가 전혀 없었습니다.
미들웨어 구현
log_data = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"source": "remote",
"tool_name": tool_name,
"parameters": context.message.arguments or {},
"success": True,
}
try:
result = await call_next(context)
log_data["duration_ms"] = (time.time() - start_time) * 1000
log_data["result_summary"] = summarize_result(result)
return result
except Exception as e:
log_data["success"] = False
log_data["error_message"] = str(e)
raise
finally:
log_db.insert_log(log_data)
핵심 설계 결정: 미들웨어는 call_next() 를 감싸서 성공과 실패 경우를 단일 finally 블록에서 모두 기록합니다. 이를 통해 예외가 발생하더라도 모든 도구 호출이 로그에 남게 됩니다.
SQLite 스키마
(스키마 정의는 간략히 생략되었습니다 – 이 테이블에는 timestamp, source, tool_name, parameters, success, duration_ms, result_summary, error_message 등과 같은 열이 포함됩니다.)
Part 2: 로컬 로그 수집 파이프라인
로그 파서
샘플 로그 라인:
2026-02-18T08:46:22Z [local-commander] Message from client: …
2026-02-18T08:46:22Z [local-commander] Message from server: …
def parse_tool_call_request(line: str) -> Optional[Dict]:
timestamp = extract_timestamp(line)
json_match = re.search(
r'Message from client: (\{.*?"id":\d+\})', line
)
msg = json.loads(json_match.group(1))
params = msg.get('params', {})
return {
'timestamp': timestamp,
'tool_name': params.get('name'),
'arguments': params.get('arguments', {}),
'request_id': msg.get('id'),
}
북마크를 이용한 증분 업로드
{
"mcp-server-local-commander.log": 1724902
}
with open(filepath, 'r', encoding='utf-8') as f:
# read new lines since last bookmark ...
@router.post("/logs/upload")
def upload_logs(logs_data: List[Dict]):
count = log_db.insert_logs_bulk(logs_data)
return {"status": "success", "uploaded_count": count}
source 필드는 통합자 역할을 하며, 대시보드가 원격 활동과 로컬 활동을 나란히 필터링하고 비교할 수 있게 해줍니다.
Part 3: 대시보드
Streamlit으로 구현되었습니다.
- 요약 카드: 총 호출 수, 성공률, 평균 응답 시간, 오류 수.
- 필터:
source(remote/local),tool_name,user_id, 시간 범위. - 드릴‑다운 테이블: 원시 로그, 오류 상세, 사용자별 API 사용량.
(스크린샷은 생략되었습니다.)
Part 4: 데이터를 조용히 삭제한 버그
증상
배포 후 데이터가 사라지기 시작했습니다.
초기 가설
보존 크론 작업이 너무 공격적이었습니다. 30 days보다 오래된 데이터만 삭제했으므로 이것이 원인은 아니었습니다.
조사
배포 파이프라인을 확인했습니다.
근본 원인
cd ~
배포 스크립트가 프로젝트 디렉터리의 모든 파일을 Git에서 재현 가능한 것으로 취급했습니다. SQLite 데이터베이스가 프로젝트 폴더 안에 있었기 때문에 각 배포 시 데이터베이스 파일이 덮어쓰기되어 모든 로그가 사라졌습니다.
This is a classic infrastructure anti‑pattern: treating stateful data the same as stateless code.
해결책: 데이터와 코드를 분리
# docker‑compose (or similar) volume mounts
volumes:
- /home/user/ai_mcp_fastmcp_remote/logs:/app/logs
- /home/user/mcp_data/db:/app/data/db
코드에서 경로 해석을 업데이트합니다:
DB_DIR = (Path(__file__).parent.parent / "data" / "db")
이제 데이터베이스가 코드베이스 외부에 존재하므로 재배포 시에도 유지됩니다.
AI 에이전트를 위한 가시성
파일에 로그를 남기는 것만으로는 충분하지 않습니다. 쿼리 가능하고, 필터링 가능하며, 환경을 초월한 가시성을 서브초 수준의 드릴다운과 함께 필요합니다. 에이전트가 8개의 도구 호출을 체인하고 그 중 6번째가 실패하면 전체 순서를 즉시 확인해야 합니다.
미들웨어 – MCP 로깅을 위한 올바른 추상화
FastMCP의 미들웨어 패턴을 사용하면 비즈니스 로직을 건드리지 않고 모든 도구 호출을 캡처할 수 있습니다.
- 한 번 등록한 하나의 클래스로 현재와 미래의 모든 도구를 포괄합니다.
상태ful 데이터와 무상태 코드 분리
이는 DevOps 101이지만, “데이터베이스”가 프로젝트 디렉터리에 있는 SQLite 파일에 불과할 때는 잊기 쉽습니다. rm -rf 명령으로 삭제될 수 있다면, 위치가 잘못된 것입니다.
북마크 패턴 – 중복 업로드 방지
모든 로그 포워딩 파이프라인에서 파일 읽기 위치를 추적하는 것은 간단하고 효과적입니다. 다음을 처리합니다:
- 증분 신규 데이터 (일반적인 경우)
- 파일 회전 (예외적인 경우)
SQLite부터 시작하기
단일 노드 MCP 배포에서는 SQLite가 적합한 선택입니다:
- 인프라 오버헤드 없음
- ACID 준수
- 필요 시 나중에 PostgreSQL로 쉽게 마이그레이션 가능
제 경우, 몇 달 동안의 전체 도구 호출 기록이 10 MB 이하의 단일 파일에 편안히 들어갑니다.
What’s Next
다음 기사에서는 이 로그들을 분석하면서 배운 AI‑agent 자율성의 실제 한계와 “human‑in‑the‑loop”가 단순한 안전 기능이 아니라 성능 최적화라는 이유를 공유하겠습니다.
- 전체 구현은 SunnyLab TV의 영상에서 시연됩니다.
- 이 기사에서 언급된 모든 코드는 GCP에서 실행되는 프로덕션 MCP 시스템에서 가져왔습니다.
- 이 시리즈의 이전 기사들은 Medium에서 찾아볼 수 있습니다.
Tags: Artificial Intelligence, Software Engineering, DevOps, Python, Cloud Computing