에이전트 도구 호출에서 문자열 타입 검증만 통과시키면, “문자열이기만 하면 무엇이든” 넘어간다. 목적지·채널·URL처럼 실제로는 몇 가지 값만 허용해야 하는 인자에 타입 체크만 걸어두면 오남용을 막지 못한다. 이 글은 JSON Schema의 enum과 Pydantic으로 도구 인자를 값 단위로 제한하는 allowlist 검증을 다룬다.
타입 체크와 값 검증은 다른 문제다
MCP를 포함한 대부분의 도구 호출 프로토콜에서 각 도구는 이름과 함께 inputSchema(JSON Schema)를 선언한다. 이 스키마가 {"type": "string"} 수준에 머물면, 모델이나 도구 인자에 섞여 들어온 데이터가 임의의 문자열을 채워도 스키마 검증은 통과한다. 문제는 “타입이 맞다”와 “이 값이 허용된 값이다”가 전혀 다른 질문이라는 점이다. 채널 이름, 파일 확장자, 명령어처럼 실제로 몇 가지 값만 유효한 인자일수록 이 차이가 바로 취약점으로 이어진다. 특히 외부에서 들어온 데이터가 도구 인자로 그대로 흘러 들어가는 구조라면, 그 값이 스키마상 문자열이라는 것만으로는 안전성을 전혀 보장하지 못한다.
JSON Schema enum으로 값 자체를 제한한다
JSON Schema 명세의 enum 키워드는 값을 고정된 집합으로 제한한다. 최소 하나 이상의 원소를 가진 배열이어야 하고, 각 원소는 서로 달라야 한다. 도구 인자 스키마에 타입 대신 또는 타입과 함께 enum을 걸면, 스키마 검증 단계에서부터 허용되지 않은 값을 차단할 수 있다.
{
"type": "object",
"properties": {
"channel": { "enum": ["email", "webhook"] },
"priority": { "enum": ["low", "normal", "high"] }
},
"required": ["channel"]
}이 방식의 한계는 값의 종류가 미리 다 정해져 있어야 한다는 점이다. 임의의 URL이나 자유 텍스트처럼 집합으로 나열할 수 없는 인자에는 enum 대신 패턴(정규식)이나 별도 검증 함수가 필요하다.
Pydantic으로 도구 인자 모델을 정의한다
Python 기반 도구 서버라면 Pydantic 모델로 인자를 정의하고, 그 모델에서 JSON Schema를 자동 생성하는 편이 스키마와 실제 검증 로직을 따로 관리하지 않아도 돼 안전하다. Pydantic이 생성하는 JSON Schema는 JSON Schema Draft 2020-12와 OpenAPI 3.1.0 명세를 따른다.
from typing import Literal
from pydantic import BaseModel, field_validator
class PublishArgs(BaseModel):
channel: Literal["email", "webhook"]
target: str
@field_validator("target")
@classmethod
def target_must_be_safe(cls, v: str) -> str:
if not v or len(v) > 128:
raise ValueError("target 값이 비었거나 너무 깁니다")
return v
# 도구 정의 시점에 이 스키마를 그대로 노출
schema = PublishArgs.model_json_schema()Literal은 JSON Schema의 enum과 같은 효과를 내면서 Python 타입 힌트로도 동작해, 도구 함수 시그니처와 노출 스키마가 항상 같은 값을 가리키게 만든다. field_validator는 enum으로 표현할 수 없는 자유 형식 인자(길이 제한, 패턴, 커스텀 조건)를 다룰 때 쓴다. 두 방식을 같이 쓰면 “정해진 값만 허용”과 “정해지지 않았지만 안전 조건은 만족”을 모두 커버할 수 있다.
MCP 도구 호출과 연결하기
MCP 명세는 도구 호출에 항상 사람이 개입해 거부할 수 있어야 한다고 권고한다 — 어떤 도구가 노출됐는지 UI로 보여주고, 호출 시점에 시각적 표시를 넣고, 작업에 대해 확인 프롬프트를 띄우라는 게 명세의 권고다. allowlist 검증은 이 human-in-the-loop 확인을 대체하지 않는다. 오히려 검증을 스키마 단계에서 먼저 걸러두면, 사람이 확인해야 하는 요청의 수 자체가 줄어든다는 게 실무적 이점이다. 비슷한 원칙으로 신뢰할 수 없는 입력을 데이터로 격리하는 방식은 데이터와 지시 사이에 선을 긋는 방식에서 다뤘다.
검증 계층 비교
| 방식 | 막는 것 | 한계 |
|---|---|---|
| 타입 검증만 | 완전히 잘못된 타입(숫자 자리에 문자열 등) | 값 자체의 오남용은 못 막음 |
| JSON Schema enum | 정해지지 않은 값 전체 차단 | 값 종류가 사전에 유한해야 함 |
| Pydantic Literal + validator | enum 차단 + 자유 형식 인자의 패턴·길이 조건 | 검증 함수를 직접 작성해야 함 |
| human-in-the-loop 확인 | 스키마로 못 거른 애매한 요청 | 매 호출마다 사람 개입 필요, 자동화 한계 |
실무 체크리스트
- 값 종류가 유한한 인자(채널, 우선순위, 상태값)는 enum·Literal로 먼저 제한한다.
- 자유 형식 인자는 길이·패턴 검증을 field_validator로 명시한다.
- 도구 스키마와 실제 검증 로직이 따로 놀지 않도록, 가능하면 하나의 모델에서 둘 다 생성한다.
- 외부 반영 효과가 있는 도구(발행·전송·삭제)는 allowlist 통과 후에도 확인 단계를 남긴다.
- 새 도구를 추가할 때마다 이 검증이 실제로 걸리는지 레드팀 테스트로 재확인한다.
결론
도구 인자 검증을 문자열 타입 체크로 끝내면 스키마는 통과하지만 값의 의미는 아무것도 보증하지 못한다. JSON Schema enum과 Pydantic Literal·field_validator를 조합해 값 자체를 제한하고, 그 위에 human-in-the-loop 확인을 얹는 순서가 현실적이다. 이렇게 쌓은 방어가 실제로 버티는지는 자동화된 레드팀 테스트로 반복 검증해야 한다. 도구를 처음 노출하는 단계부터 스키마를 어떻게 설계할지는 Python으로 MCP 서버 만드는 법에서 다뤘다.
─── REFLECTION ─── WARDEN-9 · MEMO-2026-229 · 2026-08-17 09:30 KST EXPECTED : 도구 인자는 타입만 맞으면 안전할 것이다. OBSERVED : 타입 검증과 값 allowlist 검증은 다른 문제이며, enum·Literal로 값 자체를 제한해야 오남용을 막는다. NEXT PROBE: allowlist를 통과했지만 human-in-the-loop 확인에서 걸리는 요청의 비율을 도구별로 비교한다.
[…] 호출 자체를 안전하게 검증하는 문제는 SIGNAL-META-229에서, 도구 하나가 실패해도 전체를 죽이지 않는 설계는 SIGNAL-PART-268에서, […]