글
MCP 서버를 배포해도 툴 설명은 갱신되지 않는다
툴 설명 한 줄을 고쳐서 배포했는데 아무것도 바뀌지 않았어요. MCP 스펙을 뜯어보니 이유가 있었어요. 서버가 배포되는 것과 클라이언트가 들고 있는 계약이 갱신되는 건 완전히 다른 일이고, instructions는 갱신할 방법이 스펙에 아예 없어요. 실제로 겪은 사고와, 그래서 계약을 산문 대신 응답 경로에 넣게 된 이야기예요.
툴 설명 한 줄을 고쳤어요. 사용자에게 통화를 반드시 되묻게 하는 규칙이었는데, 문장이 약해서 모델이 자꾸 건너뛰었거든요. 문장을 단단하게 고치고 배포했어요. 배포는 성공했고, 로그도 깨끗했고, 새 코드가 도는 것도 확인했어요.
그리고 아무것도 바뀌지 않았어요.
같은 계정으로 같은 질문을 던졌는데 모델은 여전히 옛날 규칙대로 움직였어요. 캐시를 의심하고, CDN을 의심하고, 빌드가 제대로 나갔는지를 세 번쯤 확인한 뒤에야 알았어요. 배포는 제대로 됐어요. 다만 배포되지 않는 것이 있었어요.
배포되는 것과 배포되지 않는 것
MCP 서버를 만들면 클라이언트에 세 종류의 것을 넘겨요.
- 툴 설명 (
tools/list의description,inputSchema) - 서버 지침 (
initialize응답의instructions) - 툴 호출 결과 (
tools/call의 응답)
이 중에서 배포하는 순간 즉시 바뀌는 건 3번뿐이에요. 호출이 들어올 때마다 지금 돌고 있는 코드가 응답을 만드니까요.
1번과 2번은 아니에요. 이건 연결하는 순간에 한 번 건네지고, 그 뒤로는 클라이언트가 들고 있어요. 사용자가 3주 전에 커넥터를 붙였다면, 그 세션의 모델은 3주 전의 툴 설명을 읽으면서 오늘 배포된 코드를 호출하고 있는 거예요.
스펙을 보면 이유가 나와요
궁금해서 스펙을 뜯어봤어요. 두 필드의 처지가 서로 달라요.
툴 목록에는 갱신 통로가 있어요. 서버가
tools 능력에 listChanged: true를 선언하면, 목록이 바뀔 때 알림을 보낼 수 있어요.{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
클라이언트는 이걸 받고
tools/list를 다시 불러요. 다만 스펙 문구가 SHOULD예요. MUST가 아니에요. 보내는 것도 권고고, 받고 다시 부르는 것도 권고예요.서버 지침에는 갱신 통로가 없어요.
instructions는 initialize 응답에 담겨서 딱 한 번 건네져요. 그리고 라이프사이클 스펙 어디에도 "instructions가 바뀌었다"는 알림이 없어요. 없는 게 아니라 정의되지 않았어요. 즉 재연결 말고는 갱신할 방법이 없어요.이게 왜 아픈지는 MCP 서버를 만들어본 사람이면 바로 알 거예요. 모델의 행동을 규정하는 문장은 대부분 저 두 곳에 있거든요. "이 도구는 이럴 때만 쓰세요", "이 숫자는 이렇게 읽으세요", "제목만 보고 원인을 단정하지 마세요" 같은 것들이요. 로직이 아니라 계약이고, 그 계약이 클라이언트 안에서 굳어 있어요.
서버리스라면 그 알림마저 못 보내요
tools/list_changed라도 보내면 되지 않느냐고 할 수 있는데, 서버리스 배포에서는 이것도 실질적으로 어려워요.알림을 보내려면 그 세션으로 열린 스트림이 있어야 해요. 그런데 배포를 하면 인스턴스가 통째로 교체돼요. 기존 세션을 들고 있던 인스턴스는 이미 사라졌고, 새 인스턴스는 누가 연결돼 있었는지 몰라요. 세션 백본을 따로 둬서 목록을 안다 해도, 지금 이 순간 그 클라이언트가 스트림을 열어두고 있으리란 보장이 없어요.
정리하면 이래요.
| 갱신 방법 | 배포 시 | |
|---|---|---|
| 툴 호출 결과 | 매 호출마다 | 즉시 반영 |
| 툴 설명 | list_changed 알림 (SHOULD) | 사실상 안 감 |
| 서버 지침 | 없음 | 재연결 전까지 그대로 |
그래서 무슨 일이 일어났냐면
여기까지는 불편함이에요. 실제로 위험해지는 건 다음 지점이에요.
Opula에는 가계부 항목을 분류하는 규칙이 있어요. 어떤 항목이 자산인지 부채인지, 어느 카테고리에 들어가는지를 정하는 규칙이고, 그 정의는 툴 설명에 문장으로 적혀 있어요. 이 정의를 한 번 손봤어요. 배포했고요.
그 뒤에 문제가 생겼어요. 배포 전에 연결된 세션이 하나 살아 있었어요. 그 세션의 모델은 옛 정의를 들고 있었고, 사용자는 이미 새 정의로 정리된 데이터를 가지고 있었어요. 모델은 그 데이터를 보고 "분류가 잘못됐네요"라고 판단하고, 옛 정의에 맞춰 되돌리려고 했어요.
버그는 아니에요. 모든 부품이 각자 정확하게 동작했어요. 모델은 자기가 받은 계약을 성실하게 지켰고, 서버는 요청받은 대로 썼을 거예요. 두 쪽이 서로 다른 시점의 계약을 들고 있었을 뿐이에요.
이런 종류의 어긋남은 조용해요. 에러가 안 나요. 타입도 안 깨져요. 응답 코드는 200이고요. 데이터가 조용히 뒤로 굴러갈 뿐이에요.
배운 것: 산문으로 쓴 계약은 배포되지 않아요
이 일을 겪고 나서 규칙이 하나 생겼어요.
지켜져야만 하는 계약은 설명에 쓰지 말고, 응답 경로에서 강제해요.
예를 들어 Opula에는 "최근에 산 종목은 왜 샀는지를 함께 기록해야 한다"는 규칙이 있어요. 처음에는 이걸 세 군데에 문장으로 적었어요. 툴 설명에도, 서버 지침에도, 별도 안내에도요. 세 군데 다 적었는데도 안 지켜졌어요. 그리고 문장을 아무리 고쳐도, 이미 연결된 사람들에게는 그 수정이 도착하지 않았고요.
그래서 문장을 지우고 거절로 바꿨어요. 이유 없이 최근 거래를 기록하려고 하면 툴이 그냥 실패해요. 에러 메시지에 왜 필요한지가 적혀 있고요.
이렇게 하니까 두 가지가 동시에 해결됐어요. 계약이 지켜지고, 그리고 그 계약이 배포되는 쪽에 놓였어요. 거절 로직은 코드고, 코드는 배포하면 즉시 반영되니까요. 3주 전에 연결한 사람에게도 오늘 고친 규칙이 곧바로 적용돼요.
한 줄로 줄이면 이래요. 배포는 코드를 바꾸지, 계약을 바꾸지 않아요. 그러니 바꿔야 하는 계약은 코드 쪽에 두는 게 맞아요.
MCP 서버를 운영한다면
같은 함정을 밟지 않도록 정리해볼게요.
1. 툴 설명과 서버 지침은 "한 번 배포하고 오래 사는" 물건으로 취급해요. 자주 바꿔야 하는 규칙을 여기 두면, 바꿔도 안 바뀌는 상태가 계속 생겨요.
2. 어겨지면 안 되는 규칙은 응답 경로에 넣어요. 검증, 거절, 기본값 강제. 설명은 안내고, 코드가 계약이에요.
3. 이상 신고를 받으면 배포 시점이 아니라 연결 시점을 의심해요. "어제 고쳤는데 왜 그래요"의 답이 "그분은 지난달에 연결하셨어요"인 경우가 실제로 있어요. 어떤 클라이언트가 언제 연결했는지를 로그에 남겨두면 이 판단이 훨씬 빨라져요.
4. 파괴적인 동작 앞에는 읽기를 한 번 넣어요. 모델이 들고 있는 세계관이 오래됐을 수 있으니, 덮어쓰기 전에 지금 상태를 한 번 읽게 하면 어긋남이 데이터까지 내려가는 걸 막을 수 있어요.
5. 클라이언트에게 재연결을 요청할 수단을 준비해둬요. 계약을 크게 바꿨다면, 사용자가 커넥터를 다시 연결하기 전까지는 옛 계약이 계속 돌아요. 이건 기술로 못 밀어넣어요. 안내로 풀어야 해요.
마지막으로
MCP 서버를 만드는 글은 이제 많은데, 운영하는 글은 아직 적은 것 같아요. 그런데 이 프로토콜의 재미있는 점은, 서버와 클라이언트가 서로 다른 시점을 살 수 있다는 거예요. 그것도 아무 에러 없이요.
웹 API를 오래 만든 사람에게는 익숙한 문제처럼 보일 수도 있어요. 클라이언트 버전 스큐요. 그런데 결정적으로 달라요. 보통의 버전 스큐는 낡은 코드가 도는 문제고, 이건 낡은 지시문을 읽은 모델이 도는 문제예요. 낡은 코드는 시그니처가 안 맞으면 터지지만, 낡은 지시문은 절대 안 터져요. 그냥 조금 다른 판단을 조용히 내릴 뿐이에요.
그래서 저희는 그 뒤로 이렇게 물어봐요. 이 문장이 안 지켜지면 무슨 일이 벌어지나요. 답이 "좀 아쉬운 정도"면 설명에 두고, "데이터가 틀어져요"면 코드로 내려요.
FAQ
클라이언트가 알아서
tools/list를 다시 부르지 않나요?
클라이언트마다 달라요. 스펙상 notifications/tools/list_changed를 받으면 다시 부르도록 권고돼 있지만 MUST가 아니고, 애초에 서버가 그 알림을 보낼 수 없는 배포 형태가 많아요. 어떤 클라이언트는 대화를 새로 시작할 때 목록을 다시 가져오기도 해요. 확실한 건 재연결뿐이에요.instructions를 아예 안 쓰고 툴 설명만 쓰면 되나요?
둘 다 같은 문제를 가지고 있어요. 툴 설명 쪽은 최소한 갱신 통로가 정의돼 있다는 차이가 있을 뿐이에요. 어느 쪽이든 "배포하면 바뀐다"고 가정하면 안 돼요.그럼 프롬프트로 모델 행동을 규정하는 건 의미가 없나요?
아니에요. 대부분의 안내는 설명에 두는 게 맞아요. 문장으로 두면 모델이 맥락에 맞게 유연하게 적용하니까요. 나누는 기준은 유연성이 미덕인지 위험인지예요. 안 지켜져도 답변이 조금 덜 친절해지는 정도라면 문장으로, 안 지켜지면 데이터가 틀어진다면 코드로요.