Claude Code 실전 가이드 4편입니다. 1편에서 시작하기와 CLAUDE.md, 2편에서 컨텍스트 관리, 3편에서 서브에이전트와 병렬 처리를 다뤘습니다. 이번 편은 MCP(Model Context Protocol) — 에이전트가 텍스트 대화를 넘어 제 인프라(PostgreSQL, GitLab, Grafana, 알림 큐)를 실제 도구로 직접 쓰게 만드는 이야기입니다.
MCP 가 왜 필요한가
Claude Code 는 기본적으로 파일을 읽고 쓰고 셸 명령을 실행합니다. 이것만으로도 코드 작업은 충분히 됩니다. 그런데 실제 운영 업무를 시키다 보면 금방 한계가 보입니다. "이 API 가 왜 느린지 봐줘" 라고 하면, 에이전트는 코드를 읽고 추측만 할 수 있을 뿐 실제로 지금 DB 에 락이 걸려 있는지, 최근 배포 이후 에러율이 올라갔는지는 알 방법이 없습니다. curl 로 API 를 직접 호출하게 시킬 수는 있지만, 그때마다 인증 토큰을 어떻게 넘길지, 응답을 어떻게 파싱할지를 매번 프롬프트에 설명해야 합니다.
MCP 는 이 틈을 메우는 프로토콜입니다. "여기 이런 도구들이 있고, 각각 이런 입력을 받아 이런 결과를 돌려준다"는 걸 서버가 미리 정의해두면, 에이전트는 그 도구 설명만 보고 스스로 언제 어떤 도구를 부를지 판단합니다. 제 경우로 말하면 이렇습니다.
- "뉴스 수집기 최근 에러 로그 좀 봐줘" → Grafana MCP 로 Loki 쿼리를 직접 날려서 실제 로그를 가져옵니다
- "이슈 몇 개나 밀려 있어" → GitLab MCP 로 이슈 목록을 조회합니다
- "users 테이블 스키마가 어떻게 생겼지" → PostgreSQL MCP 로
describe_table을 호출합니다 - "이 작업 끝났다고 텔레그램으로 알려줘" → 알림 큐 MCP 로 큐 테이블에 레코드를 넣습니다
핵심은 에이전트가 "무엇을 해야 하는지 설명 듣고 사람이 대신 실행"하는 게 아니라 "도구를 직접 호출해서 결과를 받아 다음 판단에 쓴다"는 점입니다. 진단 흐름 하나가 여러 턴에 걸친 사람 개입 없이 한 번에 끝납니다.
만들어 쓰고 있는 MCP 서버들
전부 하나의 mcp-servers 레포에 모아두고, 서버별로 파일을 나눠 관리합니다.
PostgreSQL 서버
서비스마다 DB 를 따로 두지 않고 PostgreSQL 17 단일 클러스터를 여러 서비스가 공유하는 구조라, 에이전트가 스키마를 뒤지거나 데이터를 확인할 일이 잦습니다. 그래서 만든 서버가 list_databases, list_schemas, list_tables, describe_table, table_stats, execute_sql 같은 도구를 제공합니다.
여기서 중요한 설계 하나는 읽기 전용과 쓰기 모드를 분리한 겁니다. 서버 코드는 하나지만 실행할 때 환경변수로 모드를 받아서, 조회 전용으로 뜬 인스턴스는 SELECT/EXPLAIN/WITH 만 통과시키고 그 외 문장은 도구 안에서 거부합니다. 반대로 쓰기 모드 인스턴스는 SELECT 계열을 거부합니다. 기본으로 등록해두는 건 조회 전용이고, 데이터를 바꿔야 하는 작업(알림 큐에 레코드를 넣는다든가)만 쓰기 모드로 명시해서 실행합니다.
여기에 하나 더 신경 쓴 게 우회 차단입니다. WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM x 같은 CTE 는 첫 키워드만 보면 조회처럼 보이지만 실제로는 삭제입니다. 그래서 WITH 로 시작하는 문장은 본문에 쓰기 키워드가 있는지까지 확인하고, 세미콜론으로 문장을 이어 붙이는 멀티스테이트먼트도 막아둡니다. 에이전트가 악의를 가질 일은 없지만, 잘못된 지시나 프롬프트 인젝션(외부에서 읽어온 텍스트에 지시문이 숨어 들어오는 공격)에 휘둘리는 경로는 도구 레벨에서 닫아두는 게 맞습니다.
GitLab 이슈·MR 서버
셀프호스팅 GitLab 을 쓰다 보니 이슈 등록·조회, MR 생성·조회, 코멘트, 파이프라인 상태 확인을 도구로 노출해뒀습니다. "모든 작업은 이슈부터"라는 작업 규율이 있는데, 이 서버 덕분에 에이전트가 대화 중에 바로 이슈를 만들고 그 번호를 브랜치명에 박은 뒤 작업을 시작하는 흐름이 끊기지 않습니다. 사람이 GitLab 웹 UI 를 열어 이슈 번호를 복사해 와서 다시 붙여넣는 왕복이 없어졌습니다.
Grafana 대시보드·로그 질의 서버
이건 직접 만들지 않고 Grafana 가 공식으로 배포하는 MCP 서버를 그대로 씁니다. 대시보드 검색, 패널 쿼리 확인, Loki 로그 질의, Prometheus 메트릭 조회 같은 도구가 있어서 "이 서비스 최근 5xx 응답이 늘었는지" 같은 질문에 에이전트가 직접 Loki 로 로그 패턴을 뒤지고 Prometheus 로 시계열을 확인해서 답할 수 있습니다. 진단할 때 사람이 Grafana 화면을 캡처해서 붙여넣는 과정이 사라진 게 제일 큰 변화입니다.
알림 큐 서버
작업 하나가 끝날 때마다 PostgreSQL 의 알림 큐 테이블에 레코드를 하나 넣으면, 별도로 도는 워커가 대기 상태인 행을 읽어 텔레그램으로 보내는 구조입니다. 이 큐에 넣고(send_notification), 보낸 내역을 조회하고(list_notifications), 잘못 넣은 걸 지우는(delete_notification) 동작을 도구로 노출해뒀습니다. 채널은 텔레그램과 ntfy 중에 고르되 기본값을 텔레그램으로 박아뒀는데, 이건 채널을 안 적었을 때 조용히 다른 데로 새는 일이 실제로 있었기 때문입니다.
덕분에 세션이 끝나고 나서 사람이 따로 확인하러 오지 않아도, 의미 있는 작업 단위가 끝날 때마다 알림이 옵니다. 다만 알림 본문을 대충 쓰면 나중에 읽어도 뭘 했는지 알 수가 없어서, 제목 한 줄과 레포·이슈별 결과 한 줄씩을 남기도록 규칙을 따로 CLAUDE.md 에 적어뒀습니다.
설계 원칙
MCP 서버를 몇 개 만들고 나서야 알게 된 것들입니다.
권한은 기본 최소, 쓰기는 명시
앞서 말한 읽기/쓰기 분리가 이 원칙의 구체적 사례입니다. 도구를 설계할 때 "혹시 몰라서" 쓰기 권한까지 다 열어두면, 에이전트가 실수를 안 하더라도 잘못된 지시나 프롬프트 인젝션에 취약해집니다. 조회용 서버와 쓰기용 서버(또는 모드)를 처음부터 분리해두고, 쓰기가 필요한 작업에만 명시적으로 그 서버를 등록하는 편이 안전합니다.
도구 설명문이 곧 프롬프트다
MCP 도구는 이름과 설명(description), 파라미터 스키마로 구성됩니다. 에이전트는 이 설명만 보고 "이 도구를 지금 써야 하나"를 판단하기 때문에, 설명이 모호하면 엉뚱한 상황에서 호출되거나 반대로 필요한 순간에 안 불립니다.
SQL 실행 도구 설명을 예로 들면, 처음엔 이렇게 적었습니다.
Run a SQL query.
이러면 조회 전용 모드로 떠 있는데도 에이전트가 UPDATE 를 시도했다가 거부당하고, 그제서야 다른 방법을 찾습니다. 지금은 제약과 반환 형태를 설명문에 그대로 적습니다.
Run a SQL query. (read mode: only SELECT/EXPLAIN/WITH allowed)
Results truncated to 200 rows.
한 줄 늘렸을 뿐인데 헛도는 호출이 줄었습니다. 도구 설명은 사람에게 주는 API 문서가 아니라 에이전트에게 주는 프롬프트라서, 언제 쓰는지·무엇을 반환하는지·어떤 제약이 있는지를 문장 안에 다 넣어두는 편이 낫습니다.
반환값은 작게
DB 조회 결과를 통째로 돌려주면 토큰을 순식간에 잡아먹습니다. 테이블 전체 덤프 대신 스키마 요약, 로그 전체 대신 패턴별 집계, 이슈 목록 전체 대신 필요한 필드만 — 이렇게 반환값을 좁히는 게 도구 설계에서 실제로 제일 손이 많이 가는 부분입니다.
가장 단순하면서 효과가 확실했던 건 행 수 상한이었습니다. SQL 실행 도구는 결과를 기본 200행에서 자르고, 잘렸다는 사실과 전체 행 수를 결과 끝에 같이 붙여 돌려줍니다. 상한이 없던 시절에는 SELECT * FROM 한 방으로 컨텍스트가 통째로 날아가는 일이 있었는데, 지금은 잘렸다는 걸 본 에이전트가 알아서 조건을 좁히거나 집계 쿼리로 바꿉니다. 도구 하나가 컨텍스트를 다 채워버리면, 정작 그 결과를 갖고 판단할 여유가 남지 않습니다.
감사 로그
에이전트가 직접 DB 를 조회하고 이슈를 만들고 알림을 보내는 만큼, 무엇을 언제 호출했는지 남는 게 중요합니다. 그래서 쓰기 계열 도구는 본체 로직 첫 줄에서 감사 로그를 남기도록 통일해뒀습니다. 서버 이름, 도구 이름, 넘어온 파라미터를 한 줄로 기록하는 헬퍼를 하나 만들어두고 쓰기 도구마다 호출하는 식입니다. 사후에 "그때 왜 이 레코드가 들어갔지"를 추적할 수 있어야 마음 놓고 쓰기 권한을 내줄 수 있습니다.
등록 스코프와 이름 규칙
MCP 서버는 등록 범위를 고를 수 있습니다. 사용자 전체에 걸어두면 어느 레포에서 세션을 열든 따라오고, 프로젝트에 걸어두면 그 레포에서만 보입니다. 프로젝트 범위 등록은 레포 루트의 .mcp.json 에 적히기 때문에 커밋해서 팀(혼자 쓰더라도 다른 머신)과 공유할 수 있습니다.
기준은 단순합니다. PostgreSQL 처럼 여러 프로젝트에서 공통으로 쓰는 인프라는 사용자 범위로 등록해서 어느 레포에서 작업하든 같은 이름으로 접근하게 하고, 특정 서비스에서만 의미 있는 서버(그 서비스의 API 를 감싼 서버 같은 것)는 그 레포에만 등록합니다. 그래야 관계없는 세션의 도구 목록이 불필요하게 길어지지 않습니다.
이름 규칙도 은근히 중요합니다. 서버 이름이 그대로 도구 이름의 접두사로 붙어서 에이전트 쪽에서는 <서버이름>__<도구이름> 형태로 보이기 때문입니다. 같은 종류의 서버를 프로젝트마다 다른 이름으로 등록해두면, 세션마다 도구 이름이 달라져서 CLAUDE.md 에 적어둔 "그 DB 서버로 조회해라" 같은 규칙이 어긋나기 시작합니다. 저는 하나의 공유 클러스터를 가리키는 PostgreSQL MCP 는 이름을 하나로 고정해두고 항상 그 이름으로만 등록합니다. 이름이 고정돼 있어야 규칙도 도구 이름을 근거로 쓸 수 있습니다.
도구가 많아질 때의 문제
MCP 서버를 하나둘 늘리다 보면 등록된 도구 개수가 꽤 늘어납니다. 이게 두 가지 문제를 만듭니다.
첫째, 도구 목록 자체가 컨텍스트를 차지합니다. 각 도구의 이름·설명·파라미터 스키마가 세션 시작부터 프롬프트에 실리기 때문에, 만들어둔 서버를 전부 전역에 등록해두면 실제 작업과 무관한 도구 설명이 컨텍스트를 갉아먹습니다. 그래서 전역에는 어디서 작업하든 쓰게 되는 서버(저는 PostgreSQL 과 GitLab 정도)만 남기고, 나머지는 그 서버가 실제로 필요한 레포의 .mcp.json 에만 적어둡니다. 판단 기준은 "이 서버를 지난 몇 세션 동안 실제로 불렀나" 하나입니다.
둘째, 도구가 많아지면 에이전트가 비슷한 도구 중 어느 걸 써야 할지 헷갈리는 경우가 생깁니다. 예를 들어 로그 조회 도구가 여러 서버에 비슷하게 있으면 엉뚱한 걸 부르거나 두 번 부르는 식의 낭비가 생깁니다. 이런 문제는 대체로 도구 설계 단계에서 역할을 확실히 나누고 설명문을 명확히 하는 것으로 완화됩니다. 서버 개수 자체를 늘리는 것보다, 있는 서버의 도구 설명을 다듬는 쪽이 개선 효과가 큽니다.
실사례: 공유 PostgreSQL 진단 흐름
가장 자주 쓰는 조합이 PostgreSQL MCP 와 GitLab MCP 입니다. 여러 서비스가 단일 클러스터를 공유하다 보니 "이 서비스가 갑자기 느려졌다"는 이야기가 나오면 원인 후보가 그 서비스 코드 밖에 있는 경우가 많습니다. 이때 흐름은 대체로 이렇게 흘러갑니다.
table_stats로 해당 스키마의 테이블별 행 수와 데이터·인덱스 크기를 한 번에 봅니다. 인덱스가 데이터보다 커졌다거나, 예상보다 행이 한 자릿수 많은 테이블이 여기서 먼저 눈에 띕니다.execute_sql로pg_stat_activity를 조회해 지금 오래 붙들려 있는 세션이나 대기 중인 락이 있는지 봅니다. 공유 클러스터라 다른 서비스의 배치가 원인인 경우가 실제로 있습니다.describe_table로 문제 테이블의 인덱스 정의를 확인하고, 쿼리 조건과 안 맞는 인덱스인지 판단합니다.- 원인이 좁혀지면 GitLab MCP 로 그 자리에서 이슈를 만듭니다. 위에서 본 수치가 이슈 본문에 그대로 들어갑니다.
여기서 사람이 하는 일은 "느리다"고 말하는 것과, 3번 다음에 나온 판단이 맞는지 확인하는 것뿐입니다. psql 을 열어 쿼리를 치고 결과를 캡처해서 대화에 붙여넣는 왕복이 사라지면서, 진단부터 이슈 등록까지가 한 세션 안에서 끊기지 않고 이어집니다. 이슈 본문에 실제 수치가 남는다는 것도 부수적인 이득입니다. 사람이 요약해서 옮기면 대개 "인덱스가 큼" 정도로 뭉개지는데, 에이전트는 조회한 값을 그대로 적어둡니다.
물론 이 흐름이 전부 자동으로 굴러가는 건 아닙니다. MR 을 만들고 실제로 머지하고 배포하는 자율주행에 가까운 워크플로는 다음 5편에서 다룹니다. 이번 편은 어디까지나 "에이전트가 내 인프라를 도구로 직접 두드릴 수 있게 되는 순간, 대화의 성격이 달라진다"는 것까지입니다.