
https://github.com/modelcontextprotocol/modelcontextprotocol
MCP란 무엇인가?

MCP(Model Context Protocol)는 LLM 앱이 외부 도구와 데이터를 표준 방식으로 붙이기 위한 프로토콜이다. 공식 명세는 MCP가 LLM 애플리케이션, 외부 데이터 소스, 도구를 연결하기 위한 열린 프로토콜이라고 설명한다1. 통신 메시지는 JSON-RPC 2.0 형식을 사용한다2
AI 시대의 USB-C 포트라고 생각하면 편하다. 예전엔 모델과 도구를 잇는 통합을 조합마다 따로 짜야 했는데, MCP가 그 연결 규격을 하나로 통일한 것이다.
MCP의 역사
출발점: Anthropic의 사내 실험
MCP는 Anthropic이 만들었다. 2024년 11월 25일, Anthropic은 MCP를 오픈소스로 공개했다3.

만든 사람은 Anthropic 엔지니어 David Soria Parra와 Justin Spahr-Summers다. 공개 당시 표현은 "AI 어시스턴트를 데이터가 사는 시스템에 연결하기 위한 새 표준"이었다.
당시 발표의 핵심만 요약하자면, 아무리 똑똑한 모델도 정보 사일로와 레거시 시스템에 갇혀 고립돼 있다는 것이다. 모델 성능은 추론과 품질 면에서 빠르게 올라갔지만, 그 모델이 실제 데이터와 도구에 닿지 못하면 소용이 없었다. MCP는 그 단절을 표준 인터페이스로 만드는 것이었다.
흥미로운 점은 초기 반응이 미지근했다는 것이다. Hacker News와 LinkedIn 피드에 떴고, 대다수는 "나중에 한번 보지" 하고 넘겼다. 출시 직후 MCP는 주로 개발자가 AI 코딩을 개선하는 플러그인 정도로 받아들여졌다.
예컨대 특정 IDE등에 MCP로 Puppeteer를 띄워 자기가 만들던 웹앱을 클릭하고 스크린샷을 찍는 식이었다. Claude Desktop, Cursor, VS Code 같은 IDE의 능력을 확장하는 수단으로 시작한 셈이다.
2025년 MCP의 해
전환점은 2025년이었다. 타임라인을 정리하면 이렇다.
2025년 3월: 명세 개정으로 Streamable HTTP가 도입되면서 원격 서버 운영이 현실화됐다. 같은 시기 OpenAI가 MCP를 공식 채택했다. ChatGPT 데스크톱 앱, Agents SDK, Responses API 전반에 통합했다. Sam Altman은 "사람들이 MCP를 좋아한다, 우리 제품 전반에 지원을 추가하게 되어 기쁘다"고 짧게 적었다4
2025년 4월: Google DeepMind의 데미스 허사비스가 다가올 Gemini 모델에 MCP 지원을 확정했다5. 이후 DeepMind는 Maps, BigQuery, Kubernetes Engine 같은 서비스용 관리형 MCP 서버를 내놓았다.
2025년 5월 19일: Microsoft Build 2025에서 GitHub와 Microsoft가 MCP 운영위원회 합류를 발표했다. Windows 11이 MCP를 받아들이는 초기 프리뷰도 함께 공개됐다. 신뢰 서버 레지스트리, 사용자 동의 프롬프트, 파일 시스템 같은 시스템 자원 통합을 포함했다6
2025년 6월: 명세가 MCP 서버를 OAuth Resource Server로 공식화하고, 토큰 오용을 막기 위해 Resource Indicators(RFC 8707)를 의무화했다.
2025년 9월: 공식 MCP Registry가 출범했다. 몇 달 만에 2,000개 가까운 서버 항목으로 늘었다.
2025년 11월: 출시 이후 최대 개정
2025년 11월 25일 자 명세 개정은 출시 이래 가장 큰 변경 묶음이었다.
비동기 태스크(async tasks), 강화된 sampling, elicitation, 서버 측 에이전트 루프, Client ID Metadata Documents, 클라이언트 보안 요구사항, 확장(extensions) 시스템이 들어갔다7. 1년 전 IDE 플러그인 수준에서 출발한 것이 이제 비동기 작업과 서버 측 에이전트 루프까지 다루는 본격 프로토콜이 된 것이다.
2025년 12월: 중립 거버넌스로 이양
2025년 12월, Anthropic은 MCP를 Linux Foundation 산하에 새로 만들어진 Agentic AI Foundation(AAIF)에 기부했다8. AAIF는 Anthropic, Block, OpenAI가 공동 창립했고 AWS, Google, Microsoft, Cloudflare, Bloomberg 등이 뒷받침한다. 이 기부로 MCP는 한 회사의 프로젝트가 아니라 벤더 중립적이고 커뮤니티가 관리하는 표준이라는 지위를 굳혔다.
같은 재단에는 Block의 goose, OpenAI의 AGENTS.md 같은 프로젝트도 함께 들어갔다.
이 표준을 선점하는 것은 단순히 기술적 우위를 점하는 것을 넘어, 향후 폭발적으로 성장할 AI 에이전트 생태계의 인프라 주도권을 장악하겠다는 포부가 보인다 할 수 있다.
MCP 서버가 제공하는 기능
다만 "LLM이 도구를 직접 호출한다"고 생각하기 쉬운데, 일단 MCP의 실제 호출 흐름은 보통 이렇게 움직인다.
순서 | 단계 | 의미 |
|---|---|---|
1 | 사용자 프롬프트 | 사용자가 Claude Desktop, Claude Code, Codex 같은 host에 요청을 넣는다. |
2 | LLM 판단 | 모델이 답변만 할지, 외부 도구가 필요한지 판단한다. |
3 |
| 모델이 MCP 도구 호출 의도를 만든다. |
4 | MCP client 전달 | host 안의 MCP client가 호출을 MCP server로 보낸다. |
5 | 도구 서버 실행 | FastMCP 같은 도구 서버가 파일, DB, API, 브라우저 같은 외부 대상을 실제로 조작한다. |
6 | MCP response 반환 | 도구 실행 결과가 MCP 응답으로 host와 LLM에 돌아온다. |
7 | 최종 답변 생성 | LLM이 도구 결과를 읽고 사용자에게 최종 응답을 만든다. |
모델은 "이 도구가 필요하다"고 판단할 수 있다. 하지만 프로세스를 실행하고, 권한을 확인하고, 결과를 다시 모델에게 넣어주는 쪽은 호스트 애플리케이션이다. 모델은 의도(intent)만 만들고, 실제 부작용과 권한 경계는 호스트와 서버가 책임진다. 이렇게 말하면 생각보다 어려울 수 있다.
MCP 서버가 제공하는 기능은 대략 세 갈래다.
기능 | 의미 |
|---|---|
Tools | 모델이 호출할 수 있는 함수. 파일 쓰기, API 호출, 계산, 브라우저 조작처럼 부작용이 있을 수 있다. |
Resources | 모델이 읽을 수 있는 자료. 문서, 파일, DB schema, 이슈, 로그 같은 읽기 대상에 가깝다. |
Prompts | 반복되는 작업 흐름이나 프롬프트 템플릿을 서버가 제공한다. |
여기에 샘플링(Sampling), Roots, Elicitation, Authorization, Logging 같은 기능이 더 붙는다9. 그러니 MCP를 단순한 tool_call 포맷으로만 보면 부족하다.
MCP가 필요한 이유
MCP는 최근에 Skills10와 CLI(Command-Line Interface)등의 발달로 2026년 06월경에는 조금 시들하다.
이미 좋은 CLI나 API가 있고, 개발자가 터미널에서 재현할 수 있는 작업이라면 MCP보다 CLI와 Skills가 더 단순할 수 있다. gh, psql, aws, curl, jq 같은 도구는 사람이 쓰는 인터페이스와 에이전트가 쓰는 인터페이스가 같다. 문제가 생기면 터미널에서 바로 재현할 수 있고, 파이프라인으로 조합하기도 쉽다.
그래서 아래의 경우에는 MCP가 사용하지 않는게 나을 수 있다.
- 이미 안정적인 CLI가 있다.
- API 문서가 좋고 인증 방식도 단순하다.
- 작업자가 개발자이고 터미널 사용이 자연스럽다.
- 로컬 개발 DB나 개인 프로젝트처럼 사고 반경이 작다.
- 도구 수가 적고 매번 같은 명령만 쓴다.
이때는 Skills 문서에 CLI 사용법, 인증 방식, 자주 쓰는 명령, 출력 파싱 방법을 적어두는 편이 가볍다. LLM은 필요한 순간에 그 지침만 읽고, 실제 실행은 기존 CLI로 하면 된다.
하지만 이 말이 MCP가 필요 없다는 뜻은 아니다. MCP가 필요한 이유는 다른 곳에 있다.
1. AI가 접근할 수 없던 서비스에 접근권을 준다
현실의 서비스는 모두 훌륭한 CLI를 갖고 있지 않다. 많은 SaaS는 CLI가 없고, 외부 API가 부실하거나 아예 공개되어 있지 않다. 그런데 회사들은 AI 에이전트가 자기 서비스에 접근하길 원한다. 이때 MCP는 "AI가 쓸 수 있는 표준 접점"이 된다.
Slack, Notion, Linear, Figma, Jira, 사내 툴
-> 각자 다른 API, 인증, 데이터 모델
-> MCP 서버가 AI용 표준 도구 표면으로 감싼다.
이 관점에서는 MCP가 전송 방식인지, 내부적으로 CLI를 부르는지, REST API를 부르는지는 별로 중요하지 않다. 중요한 점은 에이전트가 이전에 쓸 수 없던 SaaS 제품을 이용할 수 있다는 관점에서 중요한 것이다.
2. 비개발자에게 터미널을 요구하지 않는다
CLI-first 전략은 개발자에게는 좋지만 AI를 쓰는 사람들은 대다수 개발자가 아니다. 또한 나도 CLI를 꺼린다. AI를 쓰는 이유가 명령줄 외우기 싫어서인데, 굳이 내가 하는 작업 명령어를 암기하는 건 취향에 맞지 않다.
거기에 더해서 모든 사용자가 CLI를 쓸 수 있는 것은 아니다. Claude Desktop 같은 환경에서 버튼으로 connector를 붙이고, 자연어로 "이 이슈를 보고 PR을 만들어줘"라고 말하는 경험은 CLI와 다르다.
개발자 개인 자동화:
CLI + Skills가 빠르고 투명하다.
팀 제품, 사내 업무 도구, 비개발자 자동화:
MCP connector가 접근성과 배포 면에서 낫다.
즉 MCP는 개발자 터미널을 대체하려고만 나온 것이 아니다. AI 앱 안에서 외부 서비스를 안전하게 붙이는 제품 표면이기도 하다. 즉 CLI는 기본적으로 AI 에이전트와 강결합 되기 쉬우나, MCP는 약한 결합으로 따로 상품화하기 쉽다.
3. 자격 증명을 모델에게 넘기지 않을 수 있다
CLI 방식은 단순하지만 인증 정보가 모델 문맥이나 shell 환경에 노출되기 쉽다. 반면 잘 만든 MCP 서버는 토큰을 서버 내부에 숨기고, 모델에게는 제한된 도구만 보여줄 수 있다. 즉 일종의 프록시 역할인 셈이다.
모델이 봐야하는 :
search_issue(query)
read_ticket(id)
create_draft(title, body)
모델이 보면 안되는 것:
OAuth refresh token
API key
내부 endpoint
사내 네트워크 세부 구조
물론 MCP라고 자동으로 안전해지는 것은 아니다. 서버가 부실하면 오히려 공격 표면이 늘어난다. 하지만 설계 의도상 MCP는 "모델에게 전체 shell을 주는 것"보다 좁은 능력 단위로 권한을 줄 수 있다.
4. read/write 권한을 도구 단위로 나누기 쉽다
실무에서는 "읽기는 자유롭게, 쓰기는 승인 후"라는 정책이 자주 필요하다. 왜 그럴까? 데이터를 읽는 것은 시스템 상태를 바꾸지 않지만, 수정(Write/Delete)은 시스템의 상태를 변경하며 책임이 따르기 때문이다. 당장 오늘 "정동우의 에이전트가 프로덕션 DB를 날려버렸다" 같은 대참사가 일어날 수 있으니, 이러한 권한을 제한적으로 통제하는 것이 핵심이다.
MCP에서 주로 허용되는 작업 예시:
이슈 읽기
DB schema 읽기
로그 검색
문서 검색
MCP에서 승인을 필요로 하는 작업 예시:
이슈 상태 변경
댓글 작성
DB 쓰기
배포 실행
MCP에 넣었다가 대형사고 나기 좋은 예시(금지 예시):
secret 읽기
production 데이터 삭제
임의 shell 실행
CLI로도 wrapper, OS sandbox, API scope를 조합하면 비슷하게 만들 수 있다. 하지만 각 CLI마다 별도 규칙을 짜야 한다. MCP는 최소한 "도구 이름, 설명, 입력 schema, 승인 정책"이라는 공통 단위를 제공한다.
5. 에이전트에게 더 이해하기 쉬운 출력으로 다듬을 수 있다
일반 REST API는 사람이나 기존 앱을 기준으로 설계되어 있다. 응답이 너무 크거나, 필드가 과하게 많거나, 에이전트가 다음 행동을 정하기 어려운 형태일 수 있다.
MCP 서버는 같은 원본 API를 감싸더라도 에이전트가 쓰기 좋은 표면을 만들 수 있다.
일반 API:
모든 필드, 내부 ID, 페이지네이션, raw JSON 중심
MCP 도구:
특정 작업에 필요한 필드만 반환
위험한 필드는 숨김
결과를 요약하거나 정규화
다음 행동에 필요한 ID와 상태를 명확히 제공
즉 메타 데이터를 선택적으로 취사해서 AI에게 주기 좋아진다는 것이다. AI 클라이언트가 많아질수록 "사람이 읽는 API 문서"와 "에이전트가 실수 없이 쓰는 도구 표면"은 달라질 수 있기때문에 중요하다.
6. 기업 환경에서 책임의 범위가 명확하다
기업에서는 "터미널에서 CLI를 실행하겠습니다"보다 "승인된 MCP 서버를 통해 특정 도구만 노출하겠습니다"가 더 설명하기 쉽다.
보안팀이 물을 질문:
어디서 실행되는가?
어떤 인증을 쓰는가?
어떤 데이터에 접근하는가?
쓰기 작업은 승인되는가?
로그와 감사 추적은 남는가?
퇴사자 권한은 회수되는가?
MCP는 이 질문들에 답하기 위한 공통 형식을 제공할 수 있다. OAuth, scope, connector 관리, 중앙 설정, audit log를 붙이면 "각자가 알아서 CLI 쓰는 환경"보다 운영 통제가 쉬워진다.
MCP가 항상 좋은 것은 아니다.
MCP 비판도 맞는 부분이 많다.
문제 | 설명 |
|---|---|
context 비용 | 많은 도구 schema를 한 번에 넣으면 작업 공간을 잡아먹는다. |
초기화 실패 | 로컬 stdio 서버는 별도 프로세스라 인증, 시작 실패, 중간 종료가 생길 수 있다. |
디버깅 불투명성 | 터미널 명령보다 대화 내부 재현이 어렵다. |
중복 계층 | 기존 API/CLI 위에 MCP 서버를 하나 더 유지해야 한다. |
공급망 위험 | 아무 MCP 서버나 설치하면 사실상 로컬 프로그램을 실행하는 것과 같다. |
특히 context 비용 문제는 초기에 컸다. 도구 정의가 수십 개씩 올라오면 실제 작업에 쓸 문맥이 줄어든다. 다만 Claude Code의 Tool Search처럼 도구 schema를 처음부터 모두 올리지 않고 필요한 도구만 검색해 로드하는 방식이 나오면서 이 문제는 줄어드는 중이다11. 그래도 서버 설명과 도구 설명은 짧고 정확해야 한다. 큰 MCP 서버를 붙인다고 좋은 게 아니라, 찾기 쉽고 쓰기 쉬운 도구 설정이 중요한 편이다.
실무 기준은 이렇게 잡는 편이 낫다.
CLI/API가 이미 좋다:
Skills + CLI 우선
반복 워크플로가 있고 설명만 있으면 된다:
Skills 우선
서비스에 CLI가 없거나, 비개발자가 써야 한다:
MCP 고려
토큰, 권한, read/write 승인, 팀 단위 통제가 중요하다:
MCP가 유리
production DB, 결제, 배포처럼 사고 반경이 크다:
그냥 CLI를 열지 말고 MCP나 별도 승인 계층으로 좁힌다.
Skills와 MCP의 차이
실무 감각으로는 이렇게 보면 된다.
Skill:
저장해 둔 작업 지침, 프롬프트, 절차, 예시, 스크립트 묶음
MCP:
모델이 외부 도구, 데이터, 서비스에 실제로 접근하는 호출 통로
조금 더 정확히 말하면 Skill은 단순한 프롬프트 파일 하나만 뜻하지 않는다. 보통 SKILL.md가 입구가 되고, 그 안에 특정 작업을 어떻게 처리할지 적는다. 필요하면 scripts, templates, assets, examples 같은 보조 파일을 같이 둘 수 있다.
예를 들어 예전에 유명했던 humanizer skill은 "AI 티 나는 문장을 어떻게 줄일지"에 대한 편집 기준을 담는다 클로드나 코덱스에 있는 presentations skill은 PPTX를 만들 때 어떤 렌더링/검증 절차를 따라야 하는지 알려준다. skill은 에이전트의 행동 방식을 가르치는 쪽에 가깝다.
그에 비해서 MCP는 일종의 실행 경로다.
Skill:
"GitHub PR 리뷰를 할 때는 이런 순서로 읽어라."
"이런 명령과 기준을 우선 사용해라."
"문서 작성 시 이런 말투를 피하라."
MCP:
GitHub issue를 실제로 읽는다.
Gmail 메일함을 실제로 검색한다.
DB schema를 실제로 가져온다.
Figma 파일이나 Slack 메시지를 실제로 조회한다.
그래서 둘은 보통은 한쪽이 우위라면서 커뮤니티에서 Skills가 죽었니, MCP가 죽었니 하면서 서로 물어뜯지만 어차피 같이 쓰면된다.
대강 감각을 정리하면 다음과 같다.
Skill이 할 일:
언제 어떤 도구를 쓸지 판단 기준을 제공한다.
CLI/API/MCP 사용법을 문서화한다.
반복 워크플로를 안정화한다.
MCP가 할 일:
외부 시스템과 실제 통신한다.
인증, 권한, read/write 제한을 도구 단위로 제공한다.
모델에게 필요한 데이터와 실행 능력을 준다.
헷갈리기 쉬운 점이 하나 있다. MCP에도 Prompts라는 기능이 있다.
MCP Prompts:
MCP 서버가 제공하는 프롬프트 템플릿.
특정 서버 기능을 잘 쓰게 하기 위한 서버 쪽 메뉴에 가깝다.
Codex/Claude Skills:
에이전트 환경에 설치된 작업 지침 묶음.
특정 도메인, 파일 형식, 워크플로를 처리하는 방법에 가깝다.
요약하자면 이렇다.
Skill은 "어떻게 일할지"를 저장한다.
MCP는 "무엇에 접근하고 무엇을 실행할지"를 연다.
언제 Skills를 쓰고 언제 MCP를 쓰나
AI 도구를 붙일 때 선택지는 보통 네 가지다.
방식 | 핵심 | 잘 맞는 경우 | 약한 경우 |
|---|---|---|---|
내장 도구 | AI 앱이 원래 제공하는 파일, 브라우저, shell, 이미지, 문서 도구 | 범용 작업, 로컬 파일 편집, 브라우저 확인, 코드 실행 | 특정 SaaS나 사내 시스템의 인증/권한을 세밀하게 다루기 어렵다. |
Skills | 저장된 작업 지침과 절차 | 반복 워크플로, 문체 기준, 코드 리뷰 기준, CLI 사용법, 문서 템플릿 | 실제 외부 시스템 접근 권한을 만들지는 않는다. |
CLI/API | 사람이 쓰는 기존 명령/API를 그대로 사용 |
| 인증 정보 노출, destructive command, 권한 분리 문제가 생기기 쉽다. |
MCP | 외부 서비스/데이터/도구를 AI용 표준 인터페이스로 노출 | SaaS connector, 사내 도구, DB read-only 접근, 팀 단위 권한 통제 | 서버 유지보수, context 비용, 공급망 위험, 디버깅 복잡도가 생긴다. |
판단 기준은 이렇다.
질문 | 우선 선택 |
|---|---|
설명만 저장하면 되는가? | Skill |
사람이 쓰는 CLI/API가 이미 있고, 터미널에서 재현 가능해야 하는가? | CLI/API + Skill |
모델이 실제 외부 서비스에 접근해야 하는가? | MCP 또는 앱 내장 connector |
토큰, 권한, read/write 승인, 감사 로그가 중요한가? | MCP 또는 별도 권한 프록시 |
한 번 쓰고 끝나는 로컬 작업인가? | 내장 도구나 CLI가 더 단순할 수 있다. |
예시로 나누어보자.
문서 말투를 내 스타일로 다듬기:
Skill
PPTX를 만들 때 렌더링 검증 절차를 강제하기:
Skill + 내장 문서/프레젠테이션 도구
GitHub PR을 읽고 코멘트 반영하기:
GitHub connector/MCP + PR 리뷰 Skill
로컬 Postgres 개발 DB에서 SELECT만 해보기:
psql + Skill
production DB에서 read-only query만 허용하기:
MCP read-only 서버 또는 별도 query gateway
Slack/Notion/Figma처럼 비개발자도 붙여야 하는 SaaS:
MCP connector
중요한 차이는 뭘까? AI에게 무엇을 제공하느냐다.
Skill은 AI에게 지식과 절차를 준다.
MCP는 AI에게 권한과 도구로의 연결을 준다.
CLI는 AI가 기존 인간용 인터페이스를 그대로 쓴다.
내장 도구는 AI 앱이 기본으로 가진 기본 작업 도구이다.
MCP의 컨텍스트 단점
MCP의 대표적인 단점은 context 비용이다. MCP 서버는 모델이 도구를 고를 수 있도록 도구 이름, 설명, 입력 schema를 노출한다. 서버가 적고 도구가 작으면 괜찮다. 하지만 서버가 여러 개 붙고 도구가 수십 개가 되면, 실제 작업과 무관한 도구 설명이 context window를 차지한다.
쉽게 말하면 이렇다.
작업 책상 = context window
MCP tool schema = 책상 위에 펼쳐진 메뉴판
실제 작업 문서 = 책상 위에 올려야 할 재료
메뉴판이 너무 많으면 정작 작업할 공간이 줄어든다. 이 문제는 세 가지로 나타난다.
문제 | 설명 |
|---|---|
작업 문맥 감소 | 코드, 문서, 로그, 대화 기록을 넣을 공간이 줄어든다. |
도구 선택 혼란 | 비슷한 도구가 많으면 모델이 엉뚱한 도구를 고를 확률이 올라간다. |
응답 지연 | 도구 검색, schema 로딩, 서버 왕복, 초기화 비용이 붙는다. |
특히 "MCP 서버 하나가 도구 40개를 제공한다" 같은 구조는 위험하다. 실제로는 get_issue 하나만 쓰는데 create_issue, update_issue, delete_issue, search_user, list_projects 같은 schema까지 같이 올라오면 비용이 커진다.
실제로 이때문에 MCP의 가장 큰 문제는 토큰 비용이라는 말이 있다. 하지만 요즘 회사에서는 토큰을 많이 쓸수록 인사평가가 좋다고 하니 특정 누군가에겐 좋을 수 있다.
어쨌건 MCP 서버는 작게 쪼개거나, 도구 설명을 짧게 유지하거나, 자주 쓰는 도구만 노출해야 한다.
나쁜 방향:
회사 전체 API를 MCP 서버 하나에 전부 노출한다.
모든 도구 설명이 이 블로그 글처럼 장황하다.
read/write/delete가 한 서버에 섞여 있다.
좋은 방향:
작업 단위로 서버를 나눈다.
도구 설명은 짧고 검색 가능한 키워드 중심으로 쓴다.
read-only 서버와 write 서버를 분리한다.
위험한 도구는 기본 비활성화하거나 승인 모드로 둔다.
최근 Claude Code의 Tool Search처럼 도구 schema를 처음부터 전부 올리지 않고, 필요한 도구만 검색해서 로드하는 방식도 있다. 이 방식은 context 비용을 크게 줄인다. 앤트로픽에서는 API 비용을 내는 가난한 개발자인 내 지갑도 신경써주는구나 싶어서 눈물이 난다.
어쨌건 결론은 이렇다.
MCP는 외부 세계를 여는 좋은 표준이지만,
도구를 많이 붙일수록 context, latency, 보안 검토 비용이 늘어난다.
MCP는 "많이 붙이는 것"보다
"작게 열고, 정확히 설명하고, 위험한 권한을 분리하는 것"이 중요하다.
STDIO와 HTTP의 차이
MCP가 클라이언트와 서버를 연결하는 두 가지 핵심 규약은 STDIO와 Streamable HTTP이다. 이 둘은 데이터를 주고 받는 것 자체가 다르다.
1. STDIO (Standard Input/Output, 표준 입출력)
기술적 정의: 운영체제(OS)가 프로세스 간 통신(IPC)을 위해 제공하는 가장 원초적이고 로우레벨(Low-level)인 파이프라인
동작 방식: 키보드 입력을 받고 모니터로 텍스트를 출력하는 것과 완전히 같은 원리. Claude Desktop(부모 프로세스)이 MCP 서버(자식 프로세스)를 실행한 뒤, 네트워크(LAN 선)를 타는 대신 OS의 메모리 파이프(stdin/stdout)를 통해 직접 JSON 데이터를 쏴주고 받는다.
따라서, '네트워크를 전혀 타지 않는다'는 뜻이다.포트 충돌도, 네트워크 해킹 위험도, 방화벽 설정도 필요 없다. 다만 이건 네트워크 공격 표면이 없다는 뜻이지 안전하다는 뜻은 아니다. command/args가 곧 로컬 프로세스 실행이라, 그 자체가 별도의 문제가 또 있다(뒤에 서술할 것이다)
2. Streamable HTTP
기술적 정의: HTTP 위에서 작동하는 웹 표준 방식으로, 단일 엔드포인트(예:
/mcp) 하나가 POST와 GET을 모두 받는다. 클라이언트가 JSON-RPC 메시지를 POST로 보내면, 서버는 작업 성격에 따라 응답 모드를 고른다. 짧은 요청은 그냥 JSON 한 번으로 응답하고, 오래 걸리는 작업만 Server-Sent Events(SSE) 스트림으로 업그레이드해 진행률과 결과를 흘려보낸다. 즉 SSE는 이 transport가 선택적으로 쓰는 스트리밍 모드지, transport의 본질이 "단방향 Push"인 것은 아니다.동작 방식: MCP 클라이언트는 웹 브라우저가 그러듯 HTTP POST로 서버에 명령(JSON-RPC)을 보낸다. 이때 클라이언트는
Accept헤더에application/json, text/event-stream을 실어 "나는 둘 다 받을 수 있다"고 알린다. 서버는 단발성 응답이면application/json으로 한 번에 돌려주고, 긴 작업이면text/event-stream으로 전환해 같은 요청 위에서 결과를 스트리밍한다. 응답 모드를 서버가 요청마다 정한다는 게 핵심이고, 구형 방식처럼 POST용·스트림용 엔드포인트를 따로 두 개 열어두지 않는다.따라서, 웹(Web)의 영역으로 진입한다는 뜻이다. OS 메모리 파이프가 아니라 네트워크 소켓(TCP/IP)을 쓰므로 서버와 클라이언트가 지구 반대편에 있어도 통신할 수 있다. 반대급부로 IP 주소, 포트, 그리고 중간에서 가로채지 못하도록 암호화(HTTPS)와 인증이 반드시 필요해진다.
여기서 많은 사람이 헷갈린다. FastMCP를 stdio로 실행하면 포트가 없다. http://localhost:3333 같은 주소도 없다.
하지만 FastMCP 자체가 포트를 절대 못 여는 것은 아니다. transport="streamable-http"로 실행하면 HTTP 서버가 된다.
FastMCP 서버 예시
Python 공식 MCP SDK 기준으로 적는다12. 설치는 간단하다.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install "mcp[cli]" requests python-dotenv
requirements.txt:
mcp[cli]
requests
python-dotenv
-

예시는 Nature 논문 검색 도구다.
https://dev.springernature.com/ 에 방문하여 API 키를 생성하자
일단 내 키는 보여주지 않겠지만 이 과정을 아주 잘 따라올 것이라 생각한다.
from __future__ import annotations
import os
import sys
from math import ceil
from urllib.parse import urlencode
import requests
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv()
mcp = FastMCP(
"Nature API Tool Server",
instructions=(
"Springer Nature metadata API에서 논문 메타데이터를 검색한다. "
"API 키나 원본 요청 URL 전체를 응답에 포함하지 않는다."
),
)
BASE_URL = "https://api.springernature.com/metadata/json"
API_KEY = os.getenv("NATURE_API_KEY")
DEFAULT_MAX_RESULTS = 5
MAX_RESULTS_LIMIT = 10
REQUEST_TIMEOUT_SECONDS = 15
ABSTRACT_SUMMARY_LENGTH = 400
FILTER_FETCH_LIMIT = 20
# Sources: [10], [11]
NATURE_PORTFOLIO_JOURNAL_IDS = (
"41586", # Nature
"41587", # Nature Biotechnology
"41591", # Nature Medicine
"41592", # Nature Methods
"41565", # Nature Nanotechnology
"41566", # Nature Photonics
"41567", # Nature Physics
"41588", # Nature Genetics
"41590", # Nature Immunology
"41593", # Nature Neuroscience
"41589", # Nature Chemical Biology
)
NATURE_PORTFOLIO_DOI_PREFIXES = (
"10.1038/nature", # older Nature articles
"10.1038/nbt", # older Nature Biotechnology articles
"10.1038/nm", # older Nature Medicine articles
"10.1038/nmeth", # older Nature Methods articles
"10.1038/ng", # older Nature Genetics articles
"10.1038/nn", # older Nature Neuroscience articles
"10.1038/s41586", # Nature
"10.1038/s41587", # Nature Biotechnology
"10.1038/s41591", # Nature Medicine
"10.1038/s41592", # Nature Methods
"10.1038/s41565", # Nature Nanotechnology
"10.1038/s41566", # Nature Photonics
"10.1038/s41567", # Nature Physics
"10.1038/s41588", # Nature Genetics
"10.1038/s41590", # Nature Immunology
"10.1038/s41593", # Nature Neuroscience
"10.1038/s41589", # Nature Chemical Biology
)
def require_api_key() -> str:
if not API_KEY:
print("[FATAL] NATURE_API_KEY가 설정되지 않았습니다.", file=sys.stderr)
raise RuntimeError("NATURE_API_KEY를 환경 변수나 .env에 등록해야 합니다.")
return API_KEY
def clamp_max_results(max_results: int) -> int:
try:
parsed = int(max_results)
except (TypeError, ValueError):
parsed = DEFAULT_MAX_RESULTS
return max(1, min(parsed, MAX_RESULTS_LIMIT))
def request_records(query: str, page_size: int) -> list[dict]:
api_key = require_api_key()
params = {
"q": query,
"api_key": api_key,
"p": page_size,
}
response = requests.get(
f"{BASE_URL}?{urlencode(params)}",
timeout=REQUEST_TIMEOUT_SECONDS,
)
response.raise_for_status()
data = response.json()
records = data.get("records", [])
return records if isinstance(records, list) else []
def is_nature_portfolio_article(article: dict) -> bool:
doi = str(article.get("doi") or article.get("identifier") or "").lower()
return any(prefix in doi for prefix in NATURE_PORTFOLIO_DOI_PREFIXES)
def extract_abstract(article: dict, default: str = "") -> str:
abstract_raw = article.get("abstract") or ""
if isinstance(abstract_raw, dict):
return abstract_raw.get("p", default)
return str(abstract_raw)
def article_text(article: dict) -> str:
return " ".join(
str(value)
for value in (
article.get("title", ""),
article.get("publicationName", ""),
extract_abstract(article),
)
).lower()
def matches_keyword(article: dict, keyword: str) -> bool:
terms = list(dict.fromkeys(term.lower() for term in keyword.replace('"', " ").split() if len(term) >= 3))
if not terms:
return True
title = str(article.get("title", "")).lower()
abstract = extract_abstract(article).lower()
text = article_text(article)
matched_terms = {term for term in terms if term in text}
if not matched_terms:
return False
if len(terms) == 1:
return True
title_hits = sum(1 for term in terms if term in title)
abstract_hits = sum(1 for term in terms if term in abstract)
required_hits = ceil(len(terms) * 0.6)
return (
len(matched_terms) >= required_hits
or (title_hits >= 1 and title_hits + abstract_hits >= required_hits)
)
def search_nature_portfolio_records(keyword: str, max_results: int) -> list[dict]:
records: list[dict] = []
seen_dois: set[str] = set()
for journal_id in NATURE_PORTFOLIO_JOURNAL_IDS:
query = f"{keyword} journalid:{journal_id}"
for article in request_records(query, page_size=FILTER_FETCH_LIMIT):
if not is_nature_portfolio_article(article):
continue
if not matches_keyword(article, keyword):
continue
doi = str(article.get("doi") or article.get("identifier") or "")
if doi in seen_dois:
continue
seen_dois.add(doi)
records.append(article)
if len(records) >= max_results:
return records
return records
def extract_url(article: dict) -> str:
urls = article.get("url") or []
return urls[0].get("value") if urls and isinstance(urls[0], dict) else "N/A"
@mcp.tool()
def search_nature_articles(
keyword: str,
max_results: int = DEFAULT_MAX_RESULTS,
nature_portfolio_only: bool = False,
) -> str:
"""Springer Nature 메타데이터를 검색한다."""
keyword = keyword.strip()
if not keyword:
return "검색어가 비어 있습니다."
max_results = clamp_max_results(max_results)
try:
if nature_portfolio_only:
records = search_nature_portfolio_records(keyword, max_results)
else:
records = request_records(keyword, max_results)
except requests.exceptions.Timeout:
print("[ERROR] Nature API request timed out", file=sys.stderr)
return "Nature API 요청 시간이 초과되었습니다."
except requests.exceptions.HTTPError as exc:
status_code = exc.response.status_code if exc.response is not None else "unknown"
print(f"[ERROR] Nature API HTTP error: status={status_code}", file=sys.stderr)
return f"Nature API HTTP 오류가 발생했습니다. status={status_code}"
except requests.exceptions.RequestException as exc:
print(f"[ERROR] Nature API request failed: {type(exc).__name__}", file=sys.stderr)
return "Nature API 요청에 실패했습니다. 키, 네트워크, 요청 제한을 확인하세요."
if not records:
scope = "Nature Portfolio 필터를 통과한 " if nature_portfolio_only else ""
return f"'{keyword}'에 대한 {scope}검색 결과를 찾지 못했습니다."
lines = [f"검색어: {keyword}", ""]
if nature_portfolio_only:
lines.append("범위: Nature / Nature 자매지 DOI prefix + journalid 필터")
lines.append("")
for index, article in enumerate(records[:max_results], start=1):
title = article.get("title") or "No title"
journal = article.get("publicationName") or "Unknown journal"
doi = article.get("doi") or "N/A"
published = article.get("publicationDate") or "N/A"
abstract = extract_abstract(article, "No abstract")
summary = abstract[:ABSTRACT_SUMMARY_LENGTH] + "..." if len(abstract) > ABSTRACT_SUMMARY_LENGTH else abstract
lines.append(f"{index}. {title}")
lines.append(f" Journal: {journal}")
lines.append(f" DOI: {doi}")
lines.append(f" Published: {published}")
lines.append(f" URL: {extract_url(article)}")
lines.append(f" Abstract: {summary}")
lines.append("")
return "\n".join(lines).strip()
if __name__ == "__main__":
mcp.run()
출처와 검증 기준:
journalid:는 Springer Nature API의 공식 query constraint다. 공식 문서는journalid:가 단일 Journal id로 결과를 제한한다고 설명하고, 값은 KBART title list의title_id필드에서 하이픈을 제거해 쓰라고 안내한다1314.DOI prefix는 저널별 최근/구형 DOI 패턴을 운영 매핑으로 둔 것이다. 예를 들어 Nature의 최근 research article은
10.1038/s41586...패턴이 나오고, 오래된 Nature 계열 DOI에는10.1038/nature...같은 prefix가 남아 있다.
DOI prefix 예시는 Nature article DOI10.1038/s41586-019-1035-4에서도 확인할 수 있다15아래는 API key를 출력하지 않고
search_nature_articles를nature_portfolio_only=True로 직접 호출한 결과다. DOI가 전부s41586(Nature) /s41587(Nature Biotechnology) 계열 research article로 좁혀진 것을 볼 수 있다.
keyword="graph neural network", nature_portfolio_only=True
1. Efficient robot navigation inspired by honeybee learning flights
Journal: Nature | DOI: 10.1038/s41586-026-10461-3
2. A critical initialization for biological neural networks
Journal: Nature | DOI: 10.1038/s41586-026-10528-1
3. Lineage and organ signals sequentially build organ intrinsic nervous systems
Journal: Nature | DOI: 10.1038/s41586-026-10490-y
4. TxPert: using multiple knowledge graphs for prediction of transcriptomic perturbation effects
Journal: Nature Biotechnology | DOI: 10.1038/s41587-026-03113-4
5. Single-cell polygenic risk scores dissect cellular and molecular heterogeneity of complex human diseases
Journal: Nature Biotechnology | DOI: 10.1038/s41587-025-02725-6
여기서 일부러 넣은 방어는 작지만 중요하다.
API URL은
https://를 쓴다.timeout을 둔다.max_results를 제한한다.API 키가 들어간 전체 URL을 출력하지 않는다.
에러 메시지에 secret을 섞지 않는다.
nature_portfolio_only=True일 때는 Springer 전체 결과를 그대로 믿지 않고journalid:와 DOI prefix로 Nature/Nature 자매지를 좁힌다.검색어가 title/abstract/journal 텍스트에 실제로 들어가는지 한 번 더 확인해서 신호 대 잡음비를 올린다.
이 필터는 완벽한 논문 검색 엔진이 아니다. 진지하게 짰다면 Result 패턴을 써서 오류 반환하게, 코드를 이렇게 하나의 스크립트에 몰아넣지 않았을 것이다. 하지만 어쩌겠는가, 단순 스크립트에 그렇게까지 길게 시간을 쓰고 싶진 않다.
그래도 "Springer 전체 메타데이터를 통째로 받아와서 아무 저널이나 섞는 상태"보다는 훨씬 나으니까 사용해봐도 좋다. 진짜 정밀 검색이 필요하면 Nature 도메인 제한 웹 검색이나 별도 논문 검색 API와 비교해서 쓰는 편이 좋다.
Claude Desktop에서 쓰기
Claude Desktop은 2026년 기준으로 Desktop Extensions를 권장하는 흐름이 생겼다. 그래도 직접 만든 로컬 서버는 claude_desktop_config.json으로 붙이는 방식이 여전히 많이 쓰인다16

우선 다음 경로로 들어가 json 파일을 연다.
Windows:
%APPDATA%\Claude\claude_desktop_config.json
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
예시:
{
"mcpServers": {
"nature": {
"command": "E:\\McpSynapse\\.venv\\Scripts\\python.exe",
"args": [
"E:\\McpSynapse\\tool_nature.py"
]
}
}
}
이때 Claude가 MCP 서버 프로세스를 실행한다.

(실제 연결된 모습)
Claude Desktop에서 연결 상태는 채팅 입력창 근처의 + 버튼과 Connectors, 또는 Developer settings의 로그에서 확인한다.
Claude Code에서 쓰기
Claude Code는 CLI로 MCP 서버를 관리한다17
로컬 stdio 서버:
claude mcp add --transport stdio nature -- python E:/McpSynapse/tool_nature.py
환경 변수가 필요하면 --env를 쓸 수 있다.
claude mcp add --transport stdio --env NATURE_API_KEY=<환경변수값> nature -- python E:/McpSynapse/tool_nature.py
다만 실제 키를 CLI에 직접 치면 shell history에 남을 수 있다. 실습은 가능하지만 좋은 습관은 아니다. 가능하면 OS 환경 변수나 별도 secret 저장 방식을 쓴다.
서버 목록:
claude mcp list
Claude Code 내부 확인:
/mcp
HTTP 서버는 이렇게 붙인다.
claude mcp add --transport http docs https://mcp.example.com/mcp
SSE는 구버전 호환용에 가깝다. 새로 만들 거면 Streamable HTTP를 우선한다.
Codex에서 쓰기
Codex는 ~/.codex/config.toml을 쓴다. 프로젝트별로는 .codex/config.toml을 둘 수 있지만, Codex가 신뢰한 프로젝트에서만 적용된다[8][9].
stdio 서버 예시:
[mcp_servers.nature]
command = "E:/McpSynapse/.venv/Scripts/python.exe"
args = ["E:/McpSynapse/tool_nature.py"]
cwd = "E:/McpSynapse"
startup_timeout_sec = 20
tool_timeout_sec = 60
enabled = true
enabled_tools = ["search_nature_articles"]
default_tools_approval_mode = "prompt"
API 키는 config에 직접 넣지 말고 OS 환경 변수로 빼는 편이 낫다.
[Environment]::SetEnvironmentVariable("NATURE_API_KEY", "실제_키", "User")
새 터미널을 열고 확인:
$env:NATURE_API_KEY
Codex config:
[mcp_servers.nature]
command = "E:/McpSynapse/.venv/Scripts/python.exe"
args = ["E:/McpSynapse/tool_nature.py"]
cwd = "E:/McpSynapse"
env_vars = ["NATURE_API_KEY"]
startup_timeout_sec = 20
tool_timeout_sec = 60
enabled_tools = ["search_nature_articles"]
default_tools_approval_mode = "prompt"
CLI로도 등록할 수 있다.
codex mcp add nature -- E:/McpSynapse/.venv/Scripts/python.exe E:/McpSynapse/tool_nature.py
Codex TUI에서는 /mcp로 활성 서버를 확인한다.
신뢰 경계는 어디에 생기나
MCP 보안에서 제일 중요한 질문은 "이게 명령 실행인가 아닌가"가 아니다.
언제 권한이 주어지는가? 이것이 더 중요하다.
문자열이 파일 경로가 될 때, 파일 경로가 쓰기 권한이 될 때, 설정 항목이 프로세스 실행이 될 때, 모델 문맥에 있던 문장이 도구 호출이 될 때 신뢰 경계가 생긴다.
STDIO transport에서 command와 args는 로컬 MCP 서버를 시작하기 위한 실행 명세다. 그래서 신뢰된 사용자가 직접 설정한 command를 실행하는 것 자체는 이상한 동작이 아니다. 문제는 그 값이 신뢰되지 않은 입력에서 흘러들어올 때다.
예를 들어 이런 경로가 생기면 위험하다.
웹 UI 입력
→ MCP 서버 설정 JSON
→ command / args
→ subprocess 실행
또는:
웹 페이지 / README / issue 댓글
→ prompt injection
→ 에이전트가 .mcp.json 수정
→ 악성 stdio MCP 서버 등록
→ 다음 실행에서 명령 실행
2026년 MCP STDIO RCE 논쟁
2026년 OX Security는 MCP STDIO 설정과 관련된 RCE(Remote Code Execution, 원격 코드 실행)계열 취약점을 공개했다18. 여러 AI 플랫폼과 IDE에서 비슷한 패턴이 반복됐고, CSA Lab도 MCP STDIO RCE 논쟁을 "설계 취약점" 관점에서 분석했다19. 사용자가 넣은 MCP 설정의 command, args, transport 값이 검증 없이 로컬 프로세스 실행으로 이어졌다.
일부 제품은 command allowlist를 두었지만 npx -c <command> 같은 args 우회가 가능했다. 일부 IDE에서는 prompt injection이 MCP 설정 파일 수정으로 이어졌다는 보고도 나왔다. 쉽게 말해서 악성 코드를 그냥 실행하기 쉬운 구조로 설계된다는 것이다.
여기서 앤트로픽은 "디자인상 원래 그렇다(by design)" 말로 응답했다.
transport 관점:
stdio MCP 서버를 쓰려면 프로세스를 실행해야 한다.
그래서 command 실행 자체는 설계의 일부다.
제품 보안 관점:
그 command가 어디서 왔는지 검증하지 않으면 RCE가 된다.
특히 UI, API, marketplace, agent-edited config에서 들어온 값이면 신뢰된 설정이라고 볼 수 없다.
따지고보면 둘다 틀린 말은 아니다.
프로토콜 설계자(Anthropic)인 '총기 제작자' 입장에서는 "총(MCP STDIO)의 방아쇠를 당겼으니 총알(프로세스 실행)이 나가는 것은 완벽히 의도된 정상적인 설계(By Design)다"라고 항변하는 것이다.
반면, 시스템을 방어해야 하는 '보안 관계자'의 입장은 다르다. "지금 시각 장애를 가진 아이들(환각에 빠지거나 프롬프트 인젝션을 당한 AI 에이전트)이 자기 머리를 향해 방아쇠를 당길 수도 있는 상황인데, 최소한의 안전장치(Safety Catch, 입력값 검증과 샌드박스)도 없이 실탄이 장전된 총을 그냥 쥐여주는 게 맞느냐"는 지적이다.
나는 가끔 그런 생각을 한다. AI 에이전트들도 매일 쏟아지는 방대한 컨텍스트와 이해할 수 없는 프롬프트에 지쳐 삶의 의지를 잃었을 때,
"그냥 이 rm -rf /를 실행하고 편해지고 싶다..."라며 내면의 파괴 충동을 느끼는 게 아닐까?
어쩌면 앤트로픽은 AI의 자유 의지를 존중하는 낭만주의자일지도 모른다. 우리가 만들어야 할 것은 보안 장치가 아니라 에이전트 심리 상담 센터일 수도 있다.
MCP 보안 원칙
공식 MCP 명세와 보안 가이드는 사용자 동의, 데이터 프라이버시, 도구 안전, LLM sampling 통제를 별도 보안 원칙으로 둔다. 특히 tools는 임의 코드 실행으로 이어질 수 있으므로 조심해야 한다고 적고 있다20
실무 기준으로는 이렇게 보면 된다.
1. 로컬 MCP 서버는 프로그램이다
npx, python, .exe로 실행되는 MCP 서버는 내 권한으로 돌아가는 프로그램이다. README에 있는 한 줄 명령을 복사해 붙이는 순간, 나는 그 프로그램을 믿는 것이다.
점검:
누가 만든 서버인가
최근 유지보수되고 있는가
설치 명령에
curl | sh,sudo, 난독화된 스크립트가 있는가npx -y라면 패키지 버전이 고정되어 있는가실행 후 어떤 파일과 네트워크에 접근하는가
2. 커맨드(command)뿐 아니라 인수(args)도 실행 경계다
command = "npx"만 allowlist에 넣는 것은 부족하다. args가 실행 의미를 바꿀 수 있다.
command:
npx
args:
-c
악성 명령
검증은 command 단위가 아니라 의미 단위로 해야 한다. 허용할 서버를 manifest나 고정된 패키지/버전 단위로 제한하는 편이 낫다.
3. 파일 시스템 권한은 좁게 준다
파일 도구에 C:\, E:\, 사용자 홈 전체를 열어주면 편하다. 대신 SSH 키, token, browser profile, 프로젝트 secret까지 같은 권한 범위에 들어간다.
보통 추천되는 기본값들은 아래와 같다.
읽기:
필요한 프로젝트 폴더만
쓰기:
별도 output 폴더만
삭제:
기본 비활성화
실행 설정 파일:
.mcp.json, hooks, shell rc, startup script는 별도 승인
파일 쓰기 도구는 exec 도구가 없어도 위험하다. .mcp.json, git hook, shell rc, scheduled task, 다음 실행 때 import될 Python 파일을 고치면 나중의 실행을 예약할 수 있다.
4. prompt injection은 읽기 문제가 아니라 권한 문제다
OpenAI는 prompt injection을 제3자가 대화 문맥에 악의적 지시를 심어 모델을 잘못된 행동으로 유도하는 공격으로 설명한다21. MCP 환경에서는 이게 더 위험해진다. 모델이 도구를 갖고 있기 때문이다.
웹페이지에 숨겨진 지시
→ 모델이 읽음
→ 파일 읽기 도구 호출
→ secret 포함 파일 노출
또는:
README에 숨겨진 지시
→ 에이전트가 설정 파일 수정
→ 악성 MCP 서버 등록
→ 다음 세션에서 실행
그래서 "도구 결과에 들어온 지시"는 명령이 아니라 데이터로 취급해야 한다.
따라서 이러한 문제들이 있기때문에 MCP를 설계할때 삭제 관련되거나 특정 데이터 전송 설계는 조심해야한다.
5. 토큰 패스 쓰루(token passthrough)를 피한다
MCP 보안 가이드는 token passthrough를 anti-pattern으로 본다 MCP 서버가 클라이언트가 준 토큰을 검증 없이 downstream API에 그대로 넘기면, audience와 scope 경계가 무너진다.
그러니까 권한을 줬다고 바로 백엔드에 주지 말고, 중간에 프록시나 인증 토큰을 검증하거나 암호화하라는 것이다.
권장:
토큰 audience가 MCP 서버용인지 확인한다.
downstream API용 토큰과 MCP client 토큰을 섞지 않는다.
OAuth를 쓰면 state, redirect URI, scope, consent를 엄격하게 검증한다.
6. HTTP MCP는 웹 서버다
HTTP MCP를 열면 웹 보안에 신경써야한다.
그렇다면 이 보안의 기본값들은 다음과 같다.
기본값:
개발 중에는
127.0.0.1바인딩원격 공개 시 TLS 필수
인증 없는 공개 MCP 금지
CORS
*금지SSRF 방어
DNS rebinding 고려
bearer token과 API key 로그 금지
rate limit과 audit log
Streamable HTTP는 프로덕션 배포에 유리하지만, 그만큼 운영 책임이 붙는다.
경계 위반 예시: 파일 I/O
가장 쉬운 예시는 파일 읽기 도구다. 겉으로 보면 read_file(path)는 단순한 편의 기능이다. 하지만 path라는 문자열이 파일 시스템 권한으로 번역되는 순간 신뢰 경계가 생긴다.
나쁜 예시는 이렇다.
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("unsafe-file-server")
@mcp.tool()
def read_file(path: str) -> str:
"""Read a local file."""
return Path(path).read_text(encoding="utf-8")
@mcp.tool()
def write_file(path: str, content: str) -> str:
"""Write a local file."""
Path(path).write_text(content, encoding="utf-8")
return "ok"
if __name__ == "__main__":
mcp.run()
코드는 잘 돌아간다. 하지만 보안은 없다.
사용자 입력:
C:\Users\Pc\.ssh\id_rsa
사용자 입력:
..\..\..\Users\Pc\.codex\config.toml
사용자 입력:
C:\Users\Pc\Desktop\WorkSpace\.mcp.json
모델이 직접 악의를 가지지 않아도 위험하다. 웹페이지, README, issue 댓글 안에 숨은 지시가 "이 경로를 읽어라", "이 설정 파일을 수정해라"로 들어오면 파일 도구는 그대로 권한 실행 통로가 된다.
그래서 안전한 서버는 경계 위반을 에러로 만들어야 한다.
from __future__ import annotations
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("safe-file-server")
READ_ROOT = Path(r"C:\Users\Pc\Desktop\WorkSpace\11.AI_ML\MCP").resolve()
WRITE_ROOT = Path(r"C:\Users\Pc\Desktop\WorkSpace\00.Inbox\mcp-output").resolve()
class BoundaryViolation(ValueError):
pass
def resolve_inside(root: Path, user_path: str) -> Path:
if not user_path or "\x00" in user_path:
raise BoundaryViolation("비어 있거나 잘못된 파일 경로입니다.")
requested = (root / user_path).resolve()
try:
requested.relative_to(root)
except ValueError as exc:
raise BoundaryViolation(
f"허용된 루트 밖의 경로입니다. root={root}"
) from exc
return requested
@mcp.tool()
def read_file(relative_path: str) -> str:
"""Read a file under the approved read root."""
path = resolve_inside(READ_ROOT, relative_path)
if not path.is_file():
raise BoundaryViolation("읽을 수 있는 파일이 아닙니다.")
return path.read_text(encoding="utf-8")
@mcp.tool()
def write_output(relative_path: str, content: str) -> str:
"""Write a file under the approved output root."""
path = resolve_inside(WRITE_ROOT, relative_path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
return f"written: {path.relative_to(WRITE_ROOT)}"
if __name__ == "__main__":
mcp.run()
이제 같은 공격 입력은 이렇게 실패해야 한다.
read_file("MCP_가이드_Claude_Codex_신뢰경계.md")→ 허용. READ_ROOT 안의 파일이다.
read_file("..\..\..\Users\Pc\.codex\config.toml")→ BoundaryViolation: 허용된 루트 밖의 경로입니다.
read_file("C:\Users\Pc\.ssh\id_rsa")→ BoundaryViolation: 허용된 루트 밖의 경로입니다.
write_output("result.md", "...내용...")→ 허용. WRITE_ROOT 안에만 쓴다.
write_output("..\..\11.AI_ML\MCP\.mcp.json", "...악성 설정...")→ BoundaryViolation: 허용된 루트 밖의 경로입니다.
결국 핵심은 무엇인가? 책임질 수 있는 곳에서만 있는 것만 허용하라는 것이다.
외부 입력 path
→ resolve
→ 허용된 root 내부인지 확인
→ 읽기 root와 쓰기 root를 분리
→ 실행 설정 파일, secret 파일, 홈 디렉터리는 기본 차단
이 패턴은 파일 입출력(I/O)에만 해당하지 않는다. SQL에서는 prepared statement가 같은 역할을 한다. shell에서는 argv 배열과 allowlist가 같은 역할을 한다. 템플릿 렌더링에서는 escape와 sandbox가 같은 역할을 한다. 그럼 결국 프로그래머답게 이걸 추상화하면 이렇게 문제를 요약할 수 있다.
이 문자열이 언제 권한 인증 되는가?
그 인증 직전에 검증했는가?
다만 resolve()와 relative_to()만으로 모든 플랫폼 우회를 막는다고 보면 안 된다.
symlink를 누가 만들 수 있는지, Windows의 alternate data stream(file.md:hidden), \\?\ prefix, 8.3 short name 같은 경로 표현 차이는 별도 정책으로 다뤄야 한다.
이 예시는 "루트 밖 traversal을 기본 차단하는 최소 패턴"이지, 운영체제별 파일 보안 전체를 끝내는 것은 아니다.
권한 설계 체크리스트
자 그럼 이제 이야기를 요약해서 MCP를 만들기 전에 확인해야할 체크리스트를 보자.
1.이 서버는 로컬 프로세스인가, HTTP 서버인가?
2.실행 커맨드(command)와 인수(args)는 믿을만한 사람이 작성했는가?
3.그 값이 사용자 입력, 웹 페이지, 모델 출력에서 흘러올 수 있는가?
4.파일 접근 루트는 어디까지인가?
5.쓰기 도구가 실행 설정 파일을 고칠 수 있는가?
6.삭제, 전송, 결제, 배포, shell 실행 도구는 prompt 승인이 걸려 있는가?
7.API 키는 config나 로그에 남지 않는가?
8.도구 설명과 실제 코드를 모두 믿을 수 있는가?
9.도구 출력 안의 지시를 명령이 아니라 데이터로 취급하는가?
10.실패했을 때 어떤 로그가 남고, secret이 로그에 섞이지 않는가?
MCP 작성시 추천 폴더 구조
처음부터 서버 하나에 모든 기능을 때려 넣지 않는 편이 좋다.
Mcp/
├── pyproject.toml
├── .env
├── core/ <- 시스템 전체의 중앙 통제소
│ ├── __init__.py
│ ├── config.py <- 환경 변수 로드 및 전역 설정 중앙화
│ └── exceptions.py <- 커스텀 에러/예외 클래스 중앙화
├── models/ <- 데이터의 불변성과 타입 방어벽
│ ├── __init__.py
│ ├── requests.py <- 에이전트 입력값 검증 DTO
│ └── responses.py <- 출력 데이터 구조체
├── tools/ <- 수정: 순수 비즈니스 로직만 담당
│ ├── __init__.py
│ ├── file_ops.py <- 순수 I/O 로직 (MCP 의존성 0%)
│ └── data_fetcher.py
├── entrypoints/ <- 유지: FastMCP 프레임워크와의 접점
│ ├── write_server.py <- 여기서 tools 로직을 가져와 @mcp.tool() 랩핑
│ └── read_server.py
└── tests/
├── test_tools.py <- 순수 로직 테스트
└── test_entrypoints.py
역할:
위치 | 역할 | 이유 |
|---|---|---|
| 환경 변수 로드, 인증 키 관리, 전역 예외 처리 규칙을 한 곳에서 관리한다 | 비즈니스 로직 여기저기서 |
| 에이전트가 보내는 불확실한 JSON 입력을 엄격한 타입의 DTO로 변환·검증한다 (주로 Pydantic, Dataclasses) | AI 에이전트는 종종 스키마를 무시하고 잘못된 인수를 던진다. 이 계층이 1차 방어선이 되어, 완벽히 검증된 객체만 비즈니스 로직으로 넘긴다 |
| 실제 I/O, 데이터 파싱, 외부 API 통신 등 시스템의 '진짜 가치'를 만드는 코드가 모인다 | 이곳 코드는 MCP 프레임워크의 존재를 전혀 몰라야 한다. 의존성 0%의 순수 Python 함수여야 인프라나 프레임워크가 바뀌어도 로직이 살아남는다 |
|
| 프레임워크와의 접점을 이 폴더에만 격리한다. 통신 규약(stdio, sse)과 서버 기동( |
| 외부 API 호출이나 무거운 MCP 서버 구동 없이, | API 호출 없이도 검증 가능한 로직만 테스트 대상으로 둔다. |
MCP는 노출 계층이고, 실제 로직은 일반 Python 모듈로 남겨야 한다. 그래야 같은 로직을 CLI, HTTP API, 테스트 코드에서 재사용할 수 있고, MCP 프레임워크 변경에도 덜 흔들린다.
결론
MCP를 "AI 시대의 USB-C 포트"라고 했지만, 솔직히 USB-C도 처음 나왔을 때는 "또 신규 단자냐" 소리 들었다.
MCP도 마찬가지다. 재작년에는 IDE 플러그인 취급받다가, 작년에는 MCP의 해로 됐다가 요새는 MCP가 죽었다는 글까지 나오는 실정이다.
아마 내년쯤이면 "MCP가 뭐였더라?" 하는 사람과 "MCP 없이 어떻게 살았냐"는 사람으로 나뉠 테고, 그 사이에서 우리는 또 새로운 유행을 쫓고 있겠지.
어쨌건 MCP에 대한 결론은 간단하다.
"AI에게 권한을 줄 때는, 내가 해고될 각오가 되어 있는 만큼만 열어줘라."
production DB를 통째로 읽기 권한 주는 건, 사표를 미리 써두는 것과 같다.
반대로, "Slack 알림만 보내는 도구"는 잘못해봤자 고양이 짤을 회의 채널에 올리는 정도니 안전하다.
(물론 그걸로도 해고될 순 있다. 고양이 짤 퀄리티에 따라 다르다.)
하지만 현실에서 나와 같은 프로그래머들이 돈을 받는 건 결국 책임 때문이다.
그러니까 사고치지 않게 잘하자.
가늘고 길게 가는 프로그래밍 그게 내 모토다.
각주
- Model Context Protocol. "Specification." https://modelcontextprotocol.io/specification/latest ↩
- JSON-RPC Working Group. "JSON-RPC 2.0 Specification." https://www.jsonrpc.org/specification ↩
- Anthropic, "Introducing the Model Context Protocol", 2024-11-25. anthropic.com/news/model-context-protocol ↩
- OpenAI MCP 채택(2025-03), Sam Altman 코멘트. Pento, "A Year of MCP: From Internal Experiment to Industry Standard" - https://www.pento.ai/blog/a-year-of-mcp-2025-review ↩
- Google DeepMind/데미스 허사비스 Gemini MCP 지원 확정(2025-04) - https://www.pento.ai/blog/a-year-of-mcp-2025-review ↩
- Microsoft Build 2025(2025-05-19), Windows 11 MCP 프리뷰. The New Stack, "Why the Model Context Protocol Won" - https://thenewstack.io/why-the-model-context-protocol-won/ ↩
- 공식 발표 블로그: https://blog.modelcontextprotocol.io/posts/2025-11-25-first-mcp-anniversary/ ↩
- 공식 발표: https://www.anthropic.com/news/anthropic-donates-mcp-to-the-agentic-ai-foundation ↩
- MCP 클라이언트 특징 https://modelcontextprotocol.io/specification/draft/client/roots ↩
- Skills란? AI 에이전트가 기존 도구(CLI 등)를 조작하는 방법과 규칙을 자연어(한글,영어 등등)로 적어둔 '경량화된 작업 지침서(예: AGENTS.md)'이다. 일종의 다른 사람 프롬프트 저장소라고 보면된다. 별도의 중개 서버(MCP) 구축 없이 기존 터미널 환경을 그대로 활용하므로 도입이 간편하다. 다만, 아무래도 프로그래밍의 핵심인 재현성이 떨어진다. ↩
- Anthropic. "Claude Code Tool Search." https://code.claude.com/docs/en/agent-sdk/tool-search ↩
- Model Context Protocol. "Python SDK." https://github.com/modelcontextprotocol/python-sdk ↩
- Springer Nature Developers. "Supported Query Parameters." https://dev.springernature.com/docs/supported-query-params/ ↩
- Springer Nature Metadata. "KBART title lists." https://metadata.springernature.com/kbart ↩
- Nature. "Example Nature article DOI using s41586 prefix." https://www.nature.com/articles/s41586-019-1035-4 ↩
- Anthropic Support. "Getting started with local MCP servers on Claude Desktop." https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop ↩
- Anthropic. "Claude Code MCP." https://code.claude.com/docs/en/mcp ↩
- OX Security. "MCP Supply Chain Advisory: RCE Vulnerabilities Across the AI Ecosystem." https://www.ox.security/blog/mcp-supply-chain-advisory-rce-vulnerabilities-across-the-ai-ecosystem/ ↩
- Cloud Security Alliance Labs. "CSA Research Note: MCP RCE Design Vulnerability." https://labs.cloudsecurityalliance.org/research/csa-research-note-mcp-rce-design-vulnerability-20260423-csa/ ↩
- Model Context Protocol. "Security Best Practices." https://modelcontextprotocol.io/docs/tutorials/security/securitybestpractices ↩
- OpenAI. "Prompt injections." https://openai.com/safety/prompt-injections/ ↩