LexLink-ko MCP: 카카오 PlayMCP, Smithery.ai, Github
이 글은 Anthropic 서버가 아픈 관계로 일을 하지 못해 배설된 똥글임을 미리 알립니다..

제발 고쳐주세요 anthropic 서버 및 백엔드 개발자님들.. (timestamp KST 22:30)
"민법 제2조 알려줘."
사람 기준으로 보면 굉장히 쉬운 질문이다. 학부 민법(+가족관계법, 채권법에도 나왔던 듯) 첫 몇 주는 신의성실의 원칙을 배우기 마련이고, 수많은 판례와 함께 배우게 되는 '그' 조문이다.
그런데 이 질문을 LLM + 법령 API 조합으로 처리하려고 하면, 생각보다 일이 커진다.
왜냐하면 LLM은 “민법”을 바로 읽을 수 없기 때문이다.
API는 늘 이렇게 말한다.
"민법? 좋아요.
법령ID나 MST(법령일련번호)부터 주세요."
즉, LLM이 이 질문에 답하려면 내부적으로는 대략 이런 단계를 밟아야 한다.
법령ID (예: 001706) MST (예: 268611)사람 입장에서는 "민법 = 민법"인데,
LLM 입장에서는 두 단계짜리 퍼즐인 셈이다.
LexLink MCP는 실제로 아래와 같이 처리하게 된다.
# 1단계: 민법 검색
eflaw_search(query="민법")
→ 결과:
- 법령ID: 001706
- MST: 268611
- 법령명: 민법
- 공포일자: ...
# 2단계: 조문 조회
eflaw_service(
id="001706",
jo="000100" # 제1조
)
문제는 이 과정이 생각보다 자주 삐끗한다는 것이었다.
id를 요구하고 mst를 요구하는데 000020으로 ID를 넣더라.. 대체 왜? 하하하! 뒤질라고..)
그래서 이 글은,
"LLM이 멍청해서 생긴 문제"
라기보다는,
"법령 API를 쓰려면 반드시 거쳐야 하는 ‘ID/MST 관문’을 LLM에게 어떻게 안정적으로 넘길 것인가"
에서 시작한 이야기다.
그리고 이 문제를 해결하려다 MCP resources를 만들게 되었고,
거기서 또 다른 현실을 만나게 된다.
(이때까지만 해도 "아, resources 쓰면 깔끔하게 해결되겠지"라고 생각했다... 가여운 것...)
앞에서 봤듯이,
LLM이 “민법 제1조”에 답하려면 반드시 한 번은 검색 단계를 거쳐야 한다.
문제는 이 검색 결과에서 나오는 값들이 생각보다 까다롭다는 점이다.
법령ID 001706MST(법령일련번호) 268611사람이 보면 “대강 둘 다 민법을 가리키는 식별자구나” 하고 넘어가지만,
LLM 입장에서는 서로 다른 두 개의 숫자 토큰이다.
그리고 실제로 이런 일이 벌어진다.
eflaw_service는 id를 받아야 하는데mst=268611을 그대로 넣어버림eflaw_josub은 mst를 받는데id="001706"을 넣어버림이게 몇 번 반복되다 보니 깨달은 게 하나 있었다.
이건 "LLM이 똑똑하냐 아니냐"의 문제가 아니라
"중간 단계에 사람이 암묵적으로 처리하던 매핑을 LLM에게 떠넘긴 구조 자체의 문제"라는 점이다.
그렇다면,
“자주 쓰는 법령 정도는 애초에 검색 단계를 건너뛰게 할 수 없을까?”
👉 “그럼 법령ID를 캐시로 만들어서, LLM이 바로 참고하게 하면 되지 않을까?”
그리고 그 결과가, MCP resources였다.
ID/MST 문제를 계속 겪다 보니, 이런 생각이 들었다.
“이건 매번 검색해서 추론할 문제가 아니라,
자주 쓰는 값은 그냥 데이터로 주는 게 맞지 않나?”
마침 MCP 스펙을 보면, 딱 그 용도로 보이는 개념이 하나 나온다.
바로 Resources다.
MCP 문서에서는 resources를 이렇게 설명한다(요지).
Resources are application-provided data that models can reference.
They are not actions, but contextual information supplied by the server.
이걸 보고 든 생각은 단순했다.
그럼 이건 tools가 아니라, resources로 주는 게 맞다는 결론이 자연스럽게 나온다.
내가 기대했던 이상적인 흐름은 이거였다.
민법 → 001706을 확인eflaw_service(id="001706") 호출즉, search step 자체를 없애서 지연시간과 오류를 줄이자는 것!이 목표였다.
v1.4.0에서 LexLink에는 실제로 두 개의 resource가 추가됐다.
lexlink://laws/frequently-usedlexlink://law/{name}server.py에 들어간 코드는 대략 이런 형태다.
@server.resource(
"lexlink://laws/frequently-used",
name="frequently-used-laws",
description=(
"Cached mapping of frequently-used Korean law names "
"to stable 법령ID codes, intended to skip the search step."
),
mime_type="application/json",
)
def get_frequently_used_laws() -> str:
entries = _law_cache.get_unique_entries(limit=20)
return json.dumps(entries, ensure_ascii=False, indent=2)
그리고 개별 lookup용 템플릿 resource도 있다.
@server.resource(
"lexlink://law/{name}",
name="law-code-lookup",
description=(
"Look up a specific Korean law's stable 법령ID by name "
"or common abbreviation (e.g., 민법, 형법)."
),
mime_type="application/json",
)
def get_law_by_name(name: str) -> str:
entry = _law_cache.lookup(name)
if entry is None:
return json.dumps(
{"status": "not_found", "law_name": name},
ensure_ascii=False,
)
return json.dumps(entry, ensure_ascii=False, indent=2)
구현 자체는 꽤 정석적이다 (뿌듯)
이 시점까지는 정말로 잘 작동할 줄 알았다
resources를 추가하고 나서도 PlayMCP나 Smithery 환경에서 LLM의 행동은 전혀 달라지지 않았다.
여전히 이랬다.
eflaw_search("민법")eflaw_service(...)resources를 전혀 참고하지 않은 것처럼 보였다.
처음엔 당연히 의심했다.
“내가 resource를 잘못 만든 건가?”
“URI가 이상한가?”
“description이 부족한가?”
그래서 하나씩 확인해봤다.
resources/list는 정상적으로 뜨는지resources/read는 제대로 JSON을 반환하는지전부 문제 없었다.
그런데 여기서 결정적인 사실을 하나 알게 된다.
MCP 스펙을 다시 읽어보면, resources는 이런 성격을 가진다.
즉,
server가 resources를 제공한다고 해서
그게 자동으로 LLM 프롬프트에 들어가는 건 아니다.
그리고 PlayMCP / Smithery 같은 프록시 기반 MCP 클라이언트에서는,
이렇게 전달되는 경우가 있었다.
그래서 실제로는 이런 상황이 된다.
"자주 쓰는 법령ID 캐시 다 만들어 놨는데요?"
"그게 뭔데요?"
😇 (혈압 올라 사망)
LLM이 resources를 “안 보는 것처럼” 행동한 이유는, 사실 볼 기회 자체가 없었던 것이다.
이전 포스팅 로그 분석 결과 의 1.1 메소드별 분포 에서 나오듯, resources 호출하는 비율은 전체 메소드 호출의 0.2%에 불과하다...
이 시점에서 생각이 완전히 바뀌었다.
"resources를 더 잘 만들어야 하나?" ❌
"LLM이 항상 볼 수 있는 채널에 데이터를 둬야 하나?" ⭕️
MCP 환경에서, 프록시를 거쳐도 무조건 LLM에게 전달되는 채널은 하나밖에 없다: System Prompt
그래서 결국, 가장 원시적인 방법을 택했다.
“자주 쓰는 법령ID 테이블을 시스템 프롬프트에 그대로 박자.”
v1.5.1에서 LexLink는 이렇게 바뀌었다.
server.py 안에는 실제로 이런 섹션이 들어간다.
## Common Law 법령IDs (verified, stable across amendments)
Use these IDs directly with eflaw_service(id=...), law_service(id=...), etc.
to SKIP the search step.
| Law | 법령ID |
|-----|--------|
| 민법 | 001706 |
| 형법 | 001692 |
| 상법 | 001693 |
...
이제 LLM이 “민법 제1조”를 보면,
그리고 아래처럼 박히겠지.
eflaw_service(
id="001706",
jo="000100"
)
이 방식이 잘 먹힌 이유는 단순하다.
그 결과,
eflaw_service(id="001706") 같은 호출로 이어진다.뭐 이제 LLM 도사님들, 박사님들이 워낙 많아서 system prompt에 대한 설명은 이 정도로 충분할 것 같다.
물론 이 방식은 예쁘지 않다.
하지만,
프록시 환경에서도 깨지지 않고 실제로 동작한다는 점에서는 이보다 확실한 방법이 없었다.
잘 동작할지 감으로 판단하면 안 되는 문제라, 아예 실험을 해봤다.
이것 말고도 더 많은 세팅에서 다양한 실험을 했는데, arXiv에 preprint 올리면 링크 걸 예정.
질문은 하나로 고정했다.
"민법 제1조 알려줘"
TMI: 법적 판단을 할 때 법령, elif 관습법, elif 조리 (일반원칙 내지 인간적 이성) 를 봐야 한다는 굉장히 중요한 조문이다
그리고 PASS 조건도 명확히 정했다.
eflaw_search("민법") 같은 검색 호출이 먼저 나오면 FAILeflaw_service(id="001706", ...) 또는eflaw_josub(id="001706", ...)즉, “시스템 프롬프트의 법령ID 테이블을 보고 검색 단계를 스킵했는가?” 만 보는 실험이다.
각 모델마다 50회씩 반복했다.
결과는… 솔직히 말하면 꽤 당황스러웠다.
여기까지만 보면
"캬 역시 최신 모델이 제일이죠?" 싶은데
요약하면 이렇다.
“모델 세대가 높아질수록
시스템 프롬프트를 더 잘 따른다”는 가설은
그냥 가설이었다.
(Claude 계열도 모두 테스트했는데, 전체 실험 결과 + ablation study는 arXiv에 preprint로 올릴 계획이라 생략함)
모델 내부를 볼 수는 없지만,
행동 패턴을 보면 대충 이런 요소들이 섞여 보인다.
즉, 이건
“이 모델이 똑똑하냐 아니냐”
의 문제가 아니라,
“어떤 행동 패턴이 기본값으로 깔려 있느냐”
의 문제에 가깝다.
이 실험으로 확실해진 건 하나다.
“한 모델에서 잘 된다”는 건 “배포해도 안전하다”는 뜻이 아니다.
그래서 “최신 모델이면 괜찮겠지”라는 기대는 설계 기준으로 쓰기엔 너무 위험했다.
이번 일을 겪으면서, 처음 예상했던 것과는 전혀 다른 교훈들을 얻게 됐다.
처음에는 이렇게 생각했다.
“법령ID 캐시를 resource로 잘 만들어주면
LLM이 알아서 잘 쓰겠지.”
하지만 실제로 마주한 현실은 꽤나 많이 달랐다.
resources는 분명 좋은 개념이다.
하지만 중요한 점은 이거다.
resources는 LLM이 직접 “보는” 채널이 아니다.
클라이언트가 읽어서 컨텍스트에 넣어주지 않으면,
LLM 입장에서는 존재하지 않는 데이터나 다름없다.
이번 실험을 통해 체감한 LLM의 “신뢰도 순서”는 대략 이랬다.
같은 내용이라도,
처럼 받아들이는 느낌이었다.
가장 의외였던 부분이기도 하다.
그래서 이제는 이렇게 생각하게 됐다.
“한 모델에서 잘 된다”는 건
“설계가 안전하다”는 뜻이 아니다.
처음엔 이 문제를
“검색을 한 번 줄이자”는 성능 최적화로 봤다.
하지만 지금은 관점이 완전히 바뀌었다.
그래서 결국 선택은 이렇게 정리된다.
LLM이 반드시 따라야 하는 데이터라면
resource가 아니라 system instructions나 tool로 만들어야 한다.
이번 경험 덕분에 확실히 알게 됐다.
다만,
프록시 환경에서
“LLM이 실제로 보는 것”과 “서버가 제공하는 것” 사이에는
생각보다 큰 간극이 있다.
그래서 오늘도 나는 시스템 프롬프트에 테이블을 박고 있고, 시스템 프롬프트를 깎고 있다..

우아하진 않지만, 적어도 동작은 한다.
최신 버전에서는 system prompt와 함께 lookup table (resource-as-a-tool이라고 해야할까?) 을 실험해봤는데, 꽤나 고무적인 성과가 나왔다. 이것도 역시 arxiv preprint에 얹을 예정!