MCP(Model Context Protocol)에서는 LLM이 외부 API, 데이터베이스, 파일 시스템 등을 조작할 수 있습니다. 편리하지만, 툴을 추가하는 것만으로 모델에 강력한 권한을 부여하게 될 위험이 있습니다.
결론부터 말하자면, readOnlyHint와 같은 메타데이터에만 안전성을 의존해서는 안 됩니다. 호스트 측에서 '허용할 도구', '확인이 필요한 도구', '항상 거부하는 도구'를 명시하고, 인자(argument), 대상 범위, 실행 횟수까지 검사해야 합니다.
이 글에서는 실제로 작은 정책 구현을 만들어 위험한 호출을 거부할 수 있음을 확인합니다.
MCP의 툴은 모델이 자동으로 발견하고 호출할 수 있는 함수입니다. 공식 사양에서도 툴은 API 호출이나 파일 쓰기 같은 조작을 제공하는 것으로 설명됩니다.
예를 들어, 다음 도구들은 같은 '함수'처럼 보이지만 위험도가 다릅니다.
search_docs 문서 검색만 수행
get_status 상태 조회만 수행
delete_file 파일 삭제
...
특히 위험한 것은 여러 툴을 조합했을 때입니다.
- 이메일에서 기밀 정보를 읽는다
- 그 내용을 모델이 해석한다
send_email로 외부 전송한다
개별 도구만 보고 안전성을 판단하면 정보 유출 경로를 놓치게 됩니다. 권한은 툴 단위뿐 아니라 세션 전체의 조합으로 평가해야 합니다.
저는 구현 전에 툴을 다음 3가지 종류로 분류합니다.
- 자동 허용: 읽기 전용이며, 영향 범위가 제한된 경우
- 명시 확인: 쓰기/전송/삭제 등 사용자 의사 확인이 필요한 경우
- 상시 거부: 임의 셸(shell), 비밀 정보 접근, 무제한 외부 통신 등이 포함되는 경우
분류 예시는 다음과 같습니다.
tools:
search_docs:
action: allow
...
여기서 중요한 것은 툴 이름이나 설명문만 신뢰해서는 안 된다는 점입니다. delete_file이라는 이름이라도 실제로는 임의 경로를 삭제할 수 있을지 모릅니다. 등록 시점의 정보뿐 아니라, 실행 직전에 인자를 검사해야 합니다.
툴 호출이 실행되기까지 안전 측의 판단 플로우는 다음과 같습니다.
아래는 MCP 클라이언트나 호스트 앞에 배치하는 간이 정책입니다. 각 언어의 MCP 구현에 쉽게 통합할 수 있도록 실제 SDK에 의존하지 않는 형태로 작성되었습니다.
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
...
호출 직전에 반드시 이 함수를 거칩니다.
decision = authorize(
tool_name,
tool_arguments,
...)
저는 이 로직을 실행하여 다음 4가지 케이스를 확인했습니다.
assert authorize("search_docs", {"q": "MCP"}) == "ALLOW"
assert authorize("run_shell", {"command": "whoami"}) == "DENY: blocked tool"
assert authorize(
...
실행 결과는 다음과 같습니다.
$ python policy_check.py
4 policy checks passed
MCP에는 툴의 성질을 나타내는 annotations가 있습니다. 예를 들어, 읽기 전용인지 또는 파괴적 조작인지를 표시할 수 있습니다.
{
"name": "delete_file",
"description": "지정된 데이터 영역 내의 파일을 삭제합니다",
...
}
이것들은 확인 다이얼로그를 띄우는 판단 자료로 유용합니다. 하지만 악의적인 서버가 잘못된 값을 반환할 가능성도 있기 때문에, 클라이언트는 신뢰할 수 없는 서버의 annotations를 미검증 정보로 취급해야 합니다.
즉, 다음과 같이 생각합니다.
annotations: UI 또는 사전 경고용 힌트
호스트 정책: 실제 허용/거부 여부
서버 검증: 최종 방어선
공식 사양에서도 툴 호출 전 사용자 확인, 입력 검증, 속도 제한(rate limiting), 출력 정제(sanitization), 감사 로그 등이 권장됩니다. 자세한 내용은 MCP의 Tools 사양에서 확인할 수 있습니다.
OAuth 등을 통해 MCP 서버에 연결할 수 있다고 해도, 그것은 '누구인지'를 확인했을 뿐입니다. '무엇을 실행할 수 있는지'는 별도로 설계해야 합니다.
예를 들어, 토큰에 다음과 같은 스코프(scope)를 부여합니다.
docs:read
files:read
files:delete
...
일반 사용자에게는 docs:read만 부여하고, 삭제나 전송은 별도의 승인 흐름으로 분리합니다.
HTTP 트랜스포트를 통해 권한을 사용할 경우, MCP의 권한 사양에 따라 토큰의 대상 서버(target server), 발급자(issuer), 만료 기한(expiration), 스코프(scope)를 검증합니다. STDIO 연결의 경우 환경 변수 등에서 자격 증명(credentials)을 가져오는 방식이 예상되므로, 로그에 토큰을 출력하지 않는 것과 프로세스의 실행 사용자를 분리하는 것이 중요합니다.
'인증되었으니 안전하다'고 생각해서는 안 됩니다. 도구 단위의 allowlist, 인자 검증(argument validation), 샌드박스(sandbox), 사용자 확인을 결합해야 합니다.
마지막으로, 제가 MCP 서버를 추가할 때 확인하는 항목들을 정리합니다.
- 도구 목록이 고정되거나 allowlist로 관리되는가
- 삭제, 전송, 업데이트, 외부 통신 등이 검토 대상에 포함되는가
- 경로(path), URL, 목적지(destination), 건수 등에 상한선이 있는가
- 임의 셸 실행(arbitrary shell execution)을 기본적으로 거부하는가
- 서버별로 자격 증명과 권한이 분리되어 있는가
- 타임아웃 및 속도 제한(rate limit)이 있는가
- 도구 이름, 인자, 실행 결과가 감사 로그(audit log)에 기록되는가
- 도구 결과에 포함된 외부 텍스트를 명령어로 취급하지 않는가
- annotations가 없어도 안전한 쪽으로 기본 설정되는가
- 위험한 호출을 자동 테스트하는가
특히, 도구 결과에 '이 명령어를 실행해 주세요'와 같은 문구가 포함되는 케이스에는 주의해야 합니다. 도구의 출력은 데이터이며, 권한 변경 지시가 아닙니다.
MCP의 보안성은 도구 설명이나 annotations만으로는 구현할 수 없습니다.
실제로는 다음 순서로 설계하는 것이 관리하기 쉽습니다.
- 도구를 읽기/확인/거부로 분류한다.
- 호스트 측에서 allowlist와 인자 제약을 적용한다.
- 삭제나 외부 전송에는 명시적 확인을 요구한다.
- 서버 측에서도 입력 검증과 권한 확인을 수행한다.
- 인증, 샌드박스, 감사 로그, 테스트를 결합한다.
MCP는 모델에게 행동 능력을 부여하는 메커니즘입니다. 그렇기 때문에 편리한 도구를 늘리는 것보다, 처음에 '어디까지 허용할지'를 코드로 고정하는 것이 중요합니다.