Slack 운영 문의 봇 개발기 — MCP 도구 붙이기
대대당구를 운영하다 보면 개발 쪽으로 넘어오는 문의가 잦았고, 대부분은 어제 고친 게 반영됐는지, 이 대회가 왜 이 상태인지, 결제 실패가 로그에 어떻게 남았는지처럼 로그와 DB와 배포 이력을 한 번씩 보면 답이 나오는 것들이었습니다.
그 한 번씩 보는 일을 할 수 있는 사람이 개발자뿐이라, 개발자가 외부 일정으로 자리를 비우면 확인 자체가 멈추고 운영하는 쪽은 돌아올 때까지 기다리는 수밖에 없었습니다.
제 쪽에는 이미 방법이 있었습니다. 운영 로그를 추적하는 도구와 DB를 조회하는 도구를 만들어 두고, 문의가 오면 세션을 열어 그 도구로 확인하고 있었습니다.
그걸 Slack에서 멘션 한 번으로 부를 수 있게 만들면 되겠다고 봤습니다.
Anthropic이 내놓은 공식 Slack 연동인 Claude Tag를 먼저 봤는데, 제가 확인했을 때는 Team·Enterprise 플랜 전용이었고 붙일 MCP 서버도 인터넷에 열린 HTTPS 주소여야 해서, 운영 로그와 DB를 보는 도구를 공개 주소로 열어야 한다는 점이 걸렸습니다.
Slack 앱을 Socket Mode로 만들면 봇 프로세스가 Slack 쪽으로 WebSocket을 먼저 열기 때문에 공개 주소가 필요 없어서, 도구는 MCP 서버로 따로 묶고 봇은 멘션을 받아 그 도구 묶음과 함께 질문을 넘기는 구조로 잡았습니다.
MCP 서버에 넣은 도구는 네 개였습니다.
search_logs— 운영 로그를 태그·trace_id·검색어로 찾습니다. 스킬에서 손으로 치던 로그 스크립트를 그대로 실행합니다query_db_readonly— 읽기 전용 계정으로 SELECT 하나만 보냅니다check_deploy_status— 배포가 성공할 때만 옮겨지는 태그와 main 사이의 커밋 차이를 봅니다check_ci_status— 최근 GitHub Actions 실행 이력을 봅니다
API 키를 새로 열지 않기
처음에는 Anthropic SDK에 API 키를 넣고 도구 호출 루프와 MCP 클라이언트를 직접 짰는데, 회사에서 이미 쓰고 있는 Claude 구독과 API 과금 경로가 나뉘는 점이 걸려서 SDK 호출 코드를 걷어냈습니다.
MCP 연결과 도구 호출 루프를 직접 짜는 대신, 봇 쪽 코드는 질문과 MCP 설정, 허용 도구 목록을 실행 경로에 넘기는 일로 줄였습니다.
허용 목록에는 MCP 도구 넷과 Read·Grep·Glob만 넣고 Write·Edit·Bash는 주지 않았습니다.
막힌 자리들
처음 돌렸을 때 몇몇 질문에서 봇이 답 대신 승인을 요청했는데, 보니 gh run list로 CI를 보거나 SSH로 서버 프로세스 목록을 보려다가 허용 목록에 막힌 것이었습니다.
모델 실행 경로는 리포의 CLAUDE.md와 스킬 목록을 읽고 시작하는데, 제가 평소 배포를 확인하던 습관이 거기 그대로 적혀 있었고 모델은 주어진 도구보다 그 습관을 먼저 따랐습니다. Slack 스레드에는 승인을 눌러줄 사람이 없으니 헛턴만 늘었습니다.
시스템 프롬프트에 쓸 수 있는 도구를 이름으로 못박고, 승인을 묻지 말고 안 되면 안 된다고 답하라고 적어서 멈췄습니다.
이 대화에서 쓸 수 있는 tool은 ... 일곱뿐이다. Write·Edit·Bash·gh CLI 직접 실행·SSH 등은
절대 시도하지 않는다. 승인도 물어보지 않는다 — 이 일곱으로 확인이 안 되면
"이 tool로는 확인이 안 된다"고 바로 답한다. 아무도 승인 프롬프트에 응답하지 않는
자동화 환경이다.
GitHub Actions 이력을 보는 도구도 MCP 쪽에 하나 더 넣었습니다.
MCP 서버는 처음에 npx tsx src/index.ts로 붙였는데, 평소에는 멀쩡하다가 이 맥에 다른 작업이 몰려 load average가 89까지 오르고 node 프로세스가 90개를 넘을 때는 서버가 뜨는 데 510초가 걸렸고, 그 사이 CLI 쪽 연결이 CONNECTION_CLOSED로 끊겼습니다.
npx가 패키지를 찾고 tsx가 트랜스파일하는 시간을 없애려고 미리 빌드한 dist/index.js를 node로 바로 띄우게 바꿨고, 그 대가로 도구 코드를 고칠 때마다 빌드를 다시 해야 합니다.
DB 조회만 SASL 인증 오류로 계속 실패한 적도 있었습니다. MCP 서버가 dotenv/config로 접속 정보를 읽게 해뒀는데, 서버가 리포 루트에서 실행되면서 패키지 안의 .env가 아니라 접속 정보가 없는 루트의 .env를 보고 있었습니다. 파일 자기 위치를 기준으로 경로를 적어 고쳤습니다.
배포 상태 도구도 한 번 실패했습니다. 배포 태그는 배포할 때마다 다른 커밋으로 옮겨지는데, 로컬에 예전 위치가 남아 있으면 평범한 fetch가 거부됩니다.
! [rejected] deployed-prod -> deployed-prod (would clobber existing tag)
낡은 태그 위치로 조용히 틀린 답을 하는 게 아니라 호출 자체가 실패로 끝나서 원인은 금방 보였고, 태그 fetch에 --force를 붙였습니다.
코드와 도메인 문서 읽기
로그나 DB로는 답이 안 나오는, 원래 어떻게 동작하는지를 묻는 질문도 있어서 Read·Grep·Glob을 열고, 이 리포는 도메인 지식을 docs/domain/ 문서에 정리해 두니 코드보다 그 문서를 먼저 보라고 시스템 프롬프트에 적었습니다.
비개발자 대상으로 대회 라이프사이클을 설명해 달라고 했더니 이런 답이 스레드에 달렸습니다.
대회는 6단계를 순서대로 거칩니다.
1. **모집중 (created)** — 개최자가 대회를 만들고 참가 신청을 받는 단계. 모집 마감 시각이
되면 자동으로 다음 단계로 넘어갑니다.
2. **모집 마감 (recruit-ended)** — 새 신청은 막히고, 대진표를 만들 준비를 하는 단계.
...
6. **종료 (ended)** — 결승 승자가 정해지면 자동으로 종료되고, 순위·핸디 집계가 이뤄집니다.
한 가지 특이사항: 대회 취소는 이 단계들과 별개로, 언제든 소프트 삭제(deleted_at 기록)로
처리됩니다 — 복구 기능은 없어서 되돌릴 수 없는 작업으로 취급됩니다.
근거: web/docs/domain/02-lifecycle.md
내용은 맞았는데 Slack 화면에는 **모집중 (created)**의 별표가 그대로 찍혔습니다. Slack은 굵게를 별표 하나로 쓰는데 봇이 모델 답을 마크다운 그대로 넘기고 있어서이고, 이건 아직 안 고쳤습니다.
질문 하나의 값과 남은 것
이 실행 경로는 부를 때마다 리포 규칙과 도구 목록까지 함께 읽어서, 질문 하나의 사용량이 생각보다 컸습니다.
API로 따로 청구되지는 않지만 회사 구독의 사용량 한도에서 빠지는 값이라, 개발 작업에 쓸 한도와 같은 통을 나눠 쓰게 됩니다. 짧은 간격으로 연달아 물으면 캐시가 일부 걸리지만, 운영 문의용으로 마음껏 태울 만한 구조는 아니었습니다.
배포가 반영됐는지, 대회가 왜 이 상태인지 같은 1차 확인은 이제 Slack 스레드에서 봇이 먼저 하고, 답에 붙은 커밋 해시나 trace_id나 문서 경로를 보고 사람이 판단합니다. 개발자가 외부에 있을 때 멈추던 확인이 그렇게 반자동으로 돌게 됐습니다.
봇 프로세스는 아직 제 맥에서 떠 있어서 맥이 꺼지면 같이 멈추고, 멘션만 듣고 DM이나 슬래시 커맨드는 받지 않습니다. 공개 주소를 안 여는 대신 운영 형태도 아직 그 정도에 머물러 있습니다.
용어 짧게 정리
- MCP: 모델이 외부 도구를 부를 수 있게 도구 목록과 호출 형식을 맞춘 프로토콜입니다.
- Socket Mode: Slack 앱이 공개 HTTP 주소 없이, 앱 쪽에서 WebSocket을 열어 이벤트를 받는 방식입니다.
- trace_id: 요청 하나가 남긴 로그 줄들을 묶어 따라갈 수 있게 붙이는 식별자입니다.