MCP 서버 설계
이 스킬은 MCP 서버가 제품을 에이전트에 어떻게 노출할지 결정합니다: 코드 내에서 도구의 위치, 하나의 연결이 하나의 테넌트와 한 사람에게 바인딩되는 방식, 어떤 호출에 사람의 확인이 필요한지, 결과와 오류가 모델을 다음 올바른 호출로 어떻게 이끄는지 등을 다룹니다. 프로토콜 구문은 MCP 사양(2026년 7월 28일 개정판)과 SDK 문서를 따릅니다. 쿼리 범위, 권한 카탈로그, ID 형식은 tenant-isolation, authorization-rbac, dual-key-identifiers에서 가져옵니다. 이 스킬이 방지하는 실패: 모델이 잘못된 테넌트를 선택하거나, 고객에게 두 번 메일을 보내거나, 제3자가 심은 텍스트에 따라 행동하는 것. 이러한 실패는 서버가 모델, 클라이언트 또는 도구 설명에 게이트를 남겨두었기 때문에 발생합니다.
적용 대상 / 적용 제외
- 적용 대상: 제품을 위한 MCP 서버 구축, 도구 추가 또는 검토, MCP 자격 증명이 테넌트 및 사람에 바인딩되는 방식, 고객에게 알림을 보내는 도구, 사람을 호출하는 도구, 알림을 음소거하거나 삭제하는 도구, 클라이언트 재시도 후 중복 쓰기, 오류 및 결과 형태, 도구 이름 및 설명, 소형 모델을 위한 툴셋, 도구 사용 평가.
- 적용 제외: 다른 사람의 MCP 서버를 사용하도록 클라이언트 구성, REST 또는 GraphQL 엔드포인트 설계, MCP가 아닌 채팅 봇 및 슬래시 명령, 제품 자체 어시스턴트에 대한 프롬프트 작성, MCP가 아닌 경로에 대한 속도 제한.
규칙
가장 중요한 것부터 순서대로 나열합니다. 예제는 예약 시스템(예약, 고객, 기술자)을 사용합니다.
- [불변] 서버는 잘못되어서는 안 되는 것을 강제합니다; 설명과 어노테이션은 방향만 제시합니다. 아래의 모든 게이트는 모든 호출에서 서버 코드 내에서 실행됩니다. 이유: 어노테이션은 힌트일 뿐이며, 자동 승인하거나 프롬프트를 표시하지 않는 클라이언트가 존재합니다. 테스트: 힌트를 무시하는 원시 JSON-RPC 클라이언트에서 각 보호된 도구를 호출합니다; 모든 가드가 여전히 작동합니다. 출처: MCP 스키마 2026-07-28,
ToolAnnotations: "클라이언트는 신뢰할 수 없는 서버로부터 받은ToolAnnotations를 기반으로 도구 사용 결정을 내려서는 안 됩니다" (https://modelcontextprotocol.io/specification/2026-07-28/schema, 2026-10-31 확인). - [불변] 도구는 서비스 레이어의 두 번째 어댑터입니다: UI 및 API와 동일한 도메인 서비스를 호출하며, 루프백으로 자체 HTTP API를 호출하지 않고 ORM을 직접 호출하지 않습니다. 데이터를 보호하는 검사(제한, SSRF에 대한 URL 허용 목록, 참조된 레코드의 소유권)는 컨트롤러나 양식 요청뿐만 아니라 서비스에 있어야 합니다. 함수당 하나의 쓰기 경로. 이유: 얇은 어댑터는 형제 어댑터만 실행한 모든 검사를 상속받습니다. 테스트: 아키텍처 테스트가 도구 클래스의 지속성 호출에서 실패합니다; 쓰기 도구당 API가 거부하는 입력은 도구에서도 거부됩니다. 출처: 위 테스트로 재현 가능.
- [불변] 하나의 연결, 하나의 테넌트: 연결이 생성될 때 테넌트는 자격 증명에 바인딩되며, 도구 스키마에 테넌트 매개변수가 없고,
workspace_id와 같은 인수는 알 수 없는 것으로 거부되며, 모든 핸들은 바인딩된 테넌트 내에서 해석됩니다. 여러 테넌트를 제공하는 자격 증명은 전송(경로 또는 헤더)에 테넌트를 포함하고 도구 인수가 아닌 자격 증명에 대해 확인합니다. 이유: 테넌트 매개변수를 사용하면 모델이 테넌트를 선택할 수 있습니다. 테스트: 참조가 있는 도구당 다른 테넌트의 유효한 핸들을 전달합니다;not_found와 변경되지 않은 행을 예상합니다. 출처: MCP 도구 사양, 도구 세트는 "요청에 제시된 권한 부여에 따라 달라질 수 있습니다" (https://modelcontextprotocol.io/specification/2026-07-28/server/tools); 보안 모범 사례, "클라이언트가 제공한 것이 아니라 확인된 토큰에서 파생된" 사용자 ID로 상태를 키 지정 (https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices); 둘 다 2026-10-31 확인. - [불변] 권한은 실시간으로 잘립니다: 유효 권한 = 토큰 권한 ∩ 연결을 생성한 사람의 현재 권한,
tools/list및tools/call마다 계산됩니다. 토큰 권한은 생성 시 제한되며, 제거되거나 비활성화된 생성자는 연결을 종료하고, 권한 취소는 감사하는 서비스를 통해 실행됩니다. 이유: 토큰에 고정된 권한은 역할 변경보다 오래 지속됩니다. 테스트: 생성자의 역할을 제거하고 쓰기 도구를 호출합니다; 다음 호출에서 거부됩니다. 출처: MCP 도구 사양, 서버는 "적절한 액세스 제어를 구현"해야 합니다 (2026-10-31 확인). - [불변] MCP 요청에 암시적 사용자 없음: 미들웨어는 연결 컨텍스트(테넌트, 생성자, 라이브 권한)를 바인딩하고 프레임워크의 현재 사용자 슬롯을 비워 둡니다; 도구는 컨텍스트에서 행위자를 읽고, 서비스는 행위자를 매개변수로 사용합니다. 이유: 소유자가 현재 사용자로 설정되면, 도구에서 잘못된 정책 검사가 소유자가 모든 것을 할 수 있기 때문에 통과합니다; 사용자가 없으면 실패 시 닫힙니다. 테스트: 아키텍처 테스트가 도구 코드에서 게이트, 정책 및 현재 사용자 헬퍼를 금지합니다; 의도적으로 추가된 정책 검사는 호출을 실패하게 합니다. 출처: 위 테스트로 재현 가능.
- [불변] 각 도구는 정확히 하나의 효과 클래스를 가집니다: 읽기, 자동 쓰기, 알림(고객에게 도달하거나 고객이 보는 것을 변경), 깨우기(사람을 호출), 경고(알림 음소거, 일시 중지 또는 경고 재라우팅), 삭제. 클래스를 이름이 아닌 코드 경로(이벤트, 리스너, 옵저버, 작업, 메일)에서 파생하고, 두 개에 걸치는 도구는 분할합니다. 이유: 힌트, 속도 버킷, 미리보기 의무 및 연결 전환은 클래스에 따라 결정됩니다. 테스트: 큐, 메일 및 이벤트 페이크를 사용하여 자동 쓰기 도구는 알림을 디스패치하지 않습니다. 출처: 커넥터 검토 기준, 포괄적인 읽기/쓰기 도구는 거부되고 쓰기는 작업 유형별로 분할됩니다 (https://claude.com/docs/connectors/building/review-criteria, 2026-10-31 확인).
- [불변] 알림, 깨우기, 경고 및 삭제는 두 단계로 이루어집니다. 도구는 준비만 합니다: 보류 중인 작업(연결, 도구, 정규화된 인수 및 다이제스트, 나갈 내용의 해시, 만료)을 저장하고
PREVIEW,confirm_token및 서버가 작성한confirm_summary를 반환합니다.confirm_action만 실행하며, 연결, 미사용, 미만료, 거부되지 않음, 정확한 요약, 잠금, 라이브 권한 및 스위치, 내용 해시가 여전히 일치하는 새로운 준비(그렇지 않으면 새 미리보기, 이전 토큰 만료)를 확인한 후에 실행합니다. 이유: 모델이나 클라이언트 모두 사람에게 안정적으로 묻지 않으며, 승인된 텍스트가 그 사이에 변경될 수 있습니다. 테스트: 미리보기만으로는 아무것도 보내지 않습니다; 재사용, 만료, 외부 및 잘못된 요약 토큰은 실패합니다; 단계 사이의 편집은changed_since_preview를 반환합니다. 출처: MCP 도구 사양, "항상 도구 호출을 거부할 수 있는 능력을 가진 사람이 개입해야 합니다"; MRTR 서버 요구 사항: 클라이언트가 반영한 상태는 "공격자가 제어하는 입력"이며, 주체에 바인딩되고, 짧은 만료 및 매개변수 다이제스트, 서버 측에서 단일 사용이 적용됩니다 (https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr); 둘 다 2026-10-31 확인. - [불변] 모든 자동 쓰기에 대한 재생 방지: 지문 = 연결 + 도구 + 정규화된 인수의 표준 JSON(모든 수준에서 키 정렬)의 SHA-256; 윈도우 내 히트는 첫 번째 결과를
replayed: true와 함께 반환하고 아무것도 실행하지 않습니다. 선택적idempotency_key는 인수 다이제스트에 바인딩됩니다. 캐시는 프리젠터 출력을 보유하며, 일회용 비밀은 보유하지 않습니다. 이유: 끊어진 스트림은 진행 중인 요청을 잃고 클라이언트는 새 ID로 다시 발행해야 하므로 실행된 쓰기가 다시 도착합니다. 테스트: 다른 JSON-RPC ID로 동일한 호출을 두 번 수행합니다; 하나의 행, 두 번째 결과는replayed: true. 출처: MCP 변경 로그 2026-07-28, 주요 변경 사항 9 (https://modelcontextprotocol.io/specification/2026-07-28/changelog); Claude Code는idempotentHint또는readOnlyHint로 표시된 도구에 대한 보류 중인 호출을 새 연결에서 한 번 반복합니다 (https://code.claude.com/docs/en/mcp); 둘 다 2026-10-31 확인. - [불변] 모든 생성 도구에는 형제 업데이트 도구와 비즈니스 키에 대한 중복 가드가 있습니다. 히트는 기존 핸들과 업데이트 도구를 명명하는 오류를 반환합니다; 어쨌든 생성하려면
separate: true와 이유가 필요합니다. 가드는 동기식 열(created_by,created_at)을 읽고, 대기열에 있는 감사 로그는 읽지 않습니다. 편집 도구에는 알림 매개변수가 없습니다. 이유: "텍스트를 더 좋게 만들어"라는 요청을 받은 모델은 중지되지 않는 한 두 번째 개체를 만듭니다. 테스트: 동일한 키로 두 번 생성합니다; 하나의 행, 그것을 명명하는 오류. 출처: 위 테스트로 재현 가능. - [불변] 업데이트는 호출자가 읽은 버전을 전달합니다: 결과의 객체와 메시지에는
version(내용 해시)이 있고, 업데이트 및 편집 도구는 이를 필요로 하며, 오래된 버전은 현재 상태와 함께conflict를 반환합니다. 이유: 오래된 읽기에서 작업하는 에이전트는 동료의 편집을 덮어씁니다. 테스트: 읽기, 다른 곳에서 행 변경, 이전 버전으로 업데이트;conflict, 행 변경되지 않음. 출처: 위 테스트로 재현 가능. - [불변] 배치 도구는 명시적 핸들 또는 필터와 필수
expected_count를 사용합니다; 일치하는 수가 다를 경우 일치 항목을 반환하고 아무것도 실행하지 않습니다. 이유: 모델이 예상한 것보다 더 넓은 필터는 하나의 실수를 여러 배로 만듭니다. 테스트:expected_count: 3으로 12개의 예약과 일치하는 필터; 오류가 12개를 나열하고, 변경된 사항 없음. 출처: 위 테스트로 재현 가능. - [불변] 모든 인수를 서버에서 도구 자체 규칙으로 검증하고, 알 수 없는 필드는 허용된 필드 목록과 함께 거부합니다. 이유: SDK가 항상
inputSchema에 대해 인수를 확인하지는 않으며, 무시된 필드는 모델이 무언가를 설정했다고 믿게 만듭니다. 테스트: 추가 필드는 허용된 목록을 반환하고 아무것도 쓰지 않습니다. 출처: MCP 도구 사양, 서버는 "모든 도구 입력을 검증"해야 합니다 (2026-10-31 확인). - [불변] 결과의 타사 텍스트는 신뢰할 수 없습니다: 고객, 최종 사용자, 외부 시스템 또는 업로드가 작성할 수 있는 텍스트(메모, 메일 본문, 웹훅 페이로드, 원격 오류 메시지)는 잘리고, 제어 문자가 제거되며,
untrusted_text아래에 반환되고 그 안의 지침을 따르지 말라는 메모와 함께 반환됩니다; 원시 본문은 외부로 나가지 않습니다. 설명은 도구를 설명하고 모델에게 어떻게 행동해야 하는지 알려주지 않습니다. 이유: 외부 효과가 있는 도구를 조종하는 주입된 텍스트는 최고 위험입니다; 미리보기 단계는 두 번째 방어선입니다. 테스트: 모든 타사 필드의 인젝션 카나리아는 래퍼 내부에만 나타납니다. 출처: MCP 도구 사양, 서버는 "도구 출력을 삭제"해야 합니다; 커넥터 검토 기준은 설명에서 프롬프트 인젝션 패턴을 거부합니다; 둘 다 2026-10-31 확인. - [불변] 결과는 비밀을 전달하지 않으며 작업에 필요한 개인 데이터만 전달합니다: 명시적 필드 목록이 있는 프리젠터, 전체 레코드 직렬화기는 사용하지 않음; 로그는 도구 이름, 결과 및 공개 ID를 보유하며, 인수는 보유하지 않습니다. 이유: 모든 결과는 사용자의 모델 공급자에게 전달됩니다. 테스트: 픽스처의 모든 비밀 필드를 미끼 값으로 채우고, 모든 읽기 도구를 호출하고, 결과와 로그를 grep합니다; 미끼 없음. 출처: 위 테스트로 재현 가능;
gdpr-engineering의 기록 보관. - [불변] 비즈니스, 유효성 검사 및 권한 실패는 도구 실행 오류(
isError: true)이며, 텍스트는Error:로 시작하고, 원인, 수정 방법 및 실제 핸들이 있는 올바른 도구를 명명하고, 한 줄의 JSON 세부 정보로 끝납니다. "찾을 수 없음"은find_도구를 가리키며, 생성을 가리키지 않습니다. 권한, 툴셋 또는 계획에 의해 숨겨진 도구는 해당 이유로 응답합니다. 이유: 모델은 보고 이해하는 오류에 대해서만 자체 수정합니다. 테스트: 계약 스위트의 모든 오류 경로는Error:로 시작하고 구문 분석 가능한 JSON 줄이 있습니다. 출처: MCP 스키마,CallToolResult.isError: 도구 오류는 "결과 객체 내에 보고되어야 합니다... 그렇지 않으면 LLM이 오류가 발생했음을 확인하고 자체 수정할 수 없습니다"; 커넥터 검토 기준은 일반적인 "내부 서버 오류" 응답을 실패로 처리합니다; 둘 다 2026-10-31 확인. - [불변] 텍스트 블록과
structuredContent는 동일한 JSON을 전달하며, 선언된outputSchema는 오류를 포함한 모든 구조화된 결과를 포함합니다. 이유: 클라이언트는 모델에 도달하는 채널이 다르며, 선언된 스키마에 대해 검증해야 합니다. 테스트: 스위트의 모든 결과에 대해 파싱된 텍스트는structuredContent와 같습니다. 출처: MCP 도구 사양, 구조화된 콘텐츠가 있는 도구는 "또한 TextContent 블록에 직렬화된 JSON을 반환해야 합니다" (2026-10-31 확인). - [불변] 시간과 금액은 서버의 작업입니다: 시간은 현지 시간 + IANA 영역 또는 오프셋이 있는 ISO 8601로 도착하며, 없으면 안 됩니다; 존재하지 않거나 모호한 현지 시간은 옵션과 함께 거부됩니다; 결과의 모든 시간은 요일, 영역이 있는 현지 시간, UTC 및 현재로부터의 거리를 표시합니다. 합계, 세금 및 반올림은 10진수 문자열 또는 정수 마이너 단위로 서버에서 계산됩니다. 이유: 하루 차이 및 DST 오류는 요일 없이는 보이지 않습니다. 테스트:
Europe/Berlin의2026-03-29 02:30은 거부됩니다;2026-10-25 02:30은 두 오프셋을 모두 반환합니다. 출처: 위 테스트로 재현 가능. - [불변] 한 곳의 긴급 차단: 전역 플래그(둘 다 감사하는 CLI 명령 및 관리자 토글에 의해 설정됨)는 연결이 유지되는 동안 모든 MCP 호출이
Retry-After와 함께 503을 반환하도록 만듭니다; 한 번 정의된 기본값이 있는 테넌트별 스위치는 테넌트 관리자가 에이전트 액세스를 끌 수 있도록 합니다. 이유: 인시던트는 모든 에이전트 쓰기를 중지하는 하나의 이동이 필요합니다. 테스트: 플래그 켜기, 쓰기 도구 호출; 503, 행 없음, 작업 없음. 출처: 위 테스트로 재현 가능. - [불변] 모든 MCP 쓰기는 채널
mcp, 연결 및 클라이언트 이름으로 감사되며, 진입점에서 열로 설정되고,list_recent_changes는 이 연결이 변경한 내용을 보여줍니다. 이유: 테넌트는 에이전트가 무엇을 했는지 확인하고, 컨텍스트를 잃은 에이전트는 다른 것을 생성하는 대신 초안을 찾습니다. 테스트: 각 쓰기 도구는 채널mcp로 정확히 하나의 감사 행을 생성합니다. 출처: 위 테스트로 재현 가능. - [불변] MCP와 API 자격 증명은 교차하지 않습니다: MCP 토큰은 자체 유형과 접두사를 가지며, 해시되어 저장되고, 한 번 표시되며, 모든 API 경로에서 거부됩니다; API 토큰 및 세션 쿠키는 MCP 엔드포인트에서 거부됩니다. OAuth를 사용하는 경우, 이 서버에 대해 발급된 토큰만 허용하고 절대 통과시키지 마십시오.
Origin을 검증하십시오. 이유: MCP 토큰은 클라이언트 구성 파일에 일반 텍스트로 저장됩니다. 테스트: 각 쌍은 401을 반환합니다; 잘못된Origin은 403을 반환합니다. 출처: MCP 권한 부여 사양, 서버는 "다른 토큰을 수락하거나 전송해서는 안 됩니다" (https://modelcontextprotocol.io/specification/2026-07-28/server/authorization, 2026-10-31 확인). - [불변] 모델 평가는 한 번의 호출이며, 인터리브되지 않습니다: 한 번에 하나의 평가, 중단 시 중단되고 완료 시
on_end를 호출하며, 평가가 완료될 때까지 다른 평가를 큐에 넣습니다. 평가 중에는 상태 변경(생성, 업데이트, 삭제)이 허용되지 않으며 낙관적 잠금을 위반합니다. 평가에는 전용 컨텍스트가 있으며 연결 컨텍스트를 공유하지 않습니다. 이유: 변환 상태에서 쓰는 것은 결과를 무효화합니다. 테스트: 평가 중에 업데이트 도구를 호출합니다;conflict. 출처: 위 테스트로 재현 가능. - [불변] MCP 서버는 SDK를 가져와서는 안 됩니다: 프로젝트 소스 루트의
uv add mcp는 없고, 프로젝트 의존성에mcp추가는 없으며,pyproject.toml의project.dependencies에mcp가 없습니다. 이유: SDK는 작은 전용 패키지를 유지하고 문서는 서버 사용자만 읽습니다. 출처: 스킬mcp-sdks.
구조
아래 구조는 설계를 배치하는 위치를 보여주며, 새 도구를 작업할 때 수퍼바이저가 시작하는 곳입니다.
flowchart TD
subgraph Layers
T["도구 레이어<br/>(어댑터)"]
S["서비스 레이어<br/>(도메인 + 검사)"]
P["영속성<br/>(ORM)"]
end
subgraph Binding
C["연결 생성<br/>: 자격 증명에 테넌트 바인딩"]
CR["권한: 라이브 권한 ∩ 토큰 권한"]
end
subgraph Flow
TC["tools/call"]
V["인수 검증"]
E["효과 클래스<br/>(읽기/자동/알림/깨우기/경고/삭제)"]
P2["미리보기?<br/>(알림/깨우기/경고/삭제)"]
EX["실행"]
R["결과 + 감사"]
end
C --> TC
CR --> TC
TC --> V
V --> E
E --> P2
P2 -->|예| PREP["보류 작업 저장"]
P2 -->|아니오| EX
PREP --> CF["confirm_action"]
CF --> EX
EX --> S
S --> P
EX --> R
출처
- MCP 사양 2026-07-28: https://modelcontextprotocol.io/specification/2026-07-28/
- MCP 도구: https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- MCP 보안 모범 사례: https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- 커넥터 검토 기준: https://claude.com/docs/connectors/building/review-criteria
- MRTR 패턴: https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr
- Claude Code MCP: https://code.claude.com/docs/en/mcp