1 Claude Code에서 MCP란 무엇인가
MCP(Model Context Protocol)는 AI 모델을 외부 도구와 데이터 소스에 연결하기 위한 표준입니다. Claude Code 내부에서는 모든 MCP 서버를 1급 도구 공급자로 취급합니다. 서버의 도구는 Write, Bash 같은 내장 도구와 같은 Tool 인터페이스로 등록되며, 이름공간만 mcp__<server>__<tool> 아래에 붙습니다.
아키텍처는 세 개의 계층으로 나뉩니다.
연결 수명 주기, 설정 로딩, OAuth 인증, 전송 구성, elicitation, 중복 제거를 담당합니다.
모든 원격 MCP 호출을 감싸는 얇은 프록시 도구입니다. 런타임에 서버의 도구 목록에서 실제 이름, 스키마, call()로 덮어씁니다.
사용자용 /mcp 슬래시 명령입니다. 재연결, 활성화/비활성화, 설정 UI를 다룹니다.
React UI 패널입니다. MCPSettings, MCPListPanel, ElicitationDialog, MCPReconnect, CapabilitiesSection이 여기에 있습니다.
2 전송 유형
services/mcp/types.ts의 타입 시스템은 지원되는 전송 방식 전체를 정의합니다. 각 방식은 설정 요구사항과 사용 사례가 서로 다릅니다.
서브프로세스 생성] B -->|sse| D[SSEClientTransport
지속적인 EventSource +
OAuth / ClaudeAuthProvider] B -->|http| E[StreamableHTTPClientTransport
같은 POST에서 JSON + SSE
OAuth / session ingress] B -->|ws| F[WebSocketTransport
ws 모듈 또는 Bun WS] B -->|sse-ide| G[SSEClientTransport
IDE 확장 전용
인증 없음] B -->|ws-ide| H[WebSocketTransport
IDE 확장용,
선택적 auth token] B -->|sdk| I[SdkControlClientTransport
In-process / 제어 메시지] B -->|in-process| J[InProcessTransport
연결된 쌍
Chrome / Computer Use] style C fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style D fill:#22201d,stroke:#6e9468,color:#b8b0a4 style E fill:#22201d,stroke:#6e9468,color:#b8b0a4 style F fill:#22201d,stroke:#b8965e,color:#b8b0a4 style G fill:#22201d,stroke:#c47a50,color:#b8b0a4 style H fill:#22201d,stroke:#c47a50,color:#b8b0a4 style I fill:#22201d,stroke:#8e82ad,color:#b8b0a4 style J fill:#22201d,stroke:#8e82ad,color:#b8b0a4
서브프로세스를 생성하고 stdin/stdout으로 통신합니다. 기본 유형이라서 type을 생략하면 여기로 갑니다. stderr는 pipe로 연결되며 64 MB로 제한됩니다.
HTTP Server-Sent Events입니다. OAuth에는 ClaudeAuthProvider를 사용합니다. GET(SSE 스트림)은 의도적으로 60초 요청 타임아웃을 건너뛰고, POST에만 타임아웃이 적용됩니다.
Streamable HTTP(MCP 2025-03-26 spec)입니다. 모든 POST에 Accept: application/json, text/event-stream를 보냅니다. OAuth와 session-ingress JWT를 지원합니다.
protocols: ['mcp']를 쓰는 WebSocket입니다. Node에서는 ws 모듈을, Bun에서는 네이티브 WS를 사용합니다. 프록시 에이전트와 mTLS를 지원합니다.
IDE 확장(VS Code 등)용 SSE 변형입니다. OAuth는 없습니다. lockfile 기반 auth token이 계획되어 있지만 아직 연결되지는 않았습니다.
IDE 확장용 WebSocket 변형입니다. 선택적인 authToken을 받고, 이를 X-Claude-Code-Ide-Authorization 헤더로 보냅니다.
SDK 프로세스로 가는 제어 메시지 브리지입니다. 도구 호출은 stdout/stdin을 통해 라우팅됩니다. 프로세스를 직접 생성하지는 않습니다.
Chrome MCP와 Computer Use에서 사용합니다. createLinkedTransportPair()가 두 개의 InProcessTransport 인스턴스를 만들고, 메시지는 스택 오버플로를 피하려고 queueMicrotask로 전달됩니다.
코드: InProcessTransport 연결 쌍
// services/mcp/InProcessTransport.ts
class InProcessTransport implements Transport {
private peer: InProcessTransport | undefined
async send(message: JSONRPCMessage): Promise<void> {
// 동기 req/resp 사이클에서 스택 깊이가 커지는 것을 막기 위해 비동기로 전달
queueMicrotask(() => { this.peer?.onmessage?.(message) })
}
}
export function createLinkedTransportPair(): [Transport, Transport] {
const a = new InProcessTransport()
const b = new InProcessTransport()
a._setPeer(b); b._setPeer(a)
return [a, b]
}
3 설정 스코프 우선순위 체인
MCP 서버 설정은 여러 소스에서 오며, 명확한 우선순위에 따라 병합됩니다. ConfigScope 타입이 이 범주들을 이름 붙입니다.
| 우선순위 | 스코프 | 소스 파일 / 위치 | 메모 |
|---|---|---|---|
| 1 최고 | enterprise |
managed-mcp.json (MDM 관리 경로) |
존재하면 사용자가 관리하는 추가/삭제를 모두 막습니다. 독점 제어입니다. |
| 2 | dynamic |
CLI 플래그 --mcp-config <path> |
시작 시 전달되며, 사용 전에 정책 필터링을 거칩니다. |
| 3 | claudeai |
Claude.ai 커넥터 API(원격 fetch) | URL 시그니처를 기준으로 수동 설정 서버와 중복 제거합니다. |
| 4 | project |
.mcp.json (CWD & 상위 디렉터리, 루트부터 아래로) |
CWD에 가장 가까운 것이 이깁니다. 상위 디렉터리도 함께 찾고, 하위 디렉터리가 상위를 덮어씁니다. |
| 5 | local |
~/.claude/projects/<hash>/ |
프로젝트별 로컬 상태이며 git에는 들어가지 않습니다. |
| 6 | user |
~/.claude/settings.json (전역 설정) |
사용자 전체 기본값입니다. |
| 7 | managed |
플러그인이 제공한 서버 | plugin:name:server 이름공간을 씁니다. 시그니처(URL 또는 command 배열)를 기준으로 수동 서버와 내용 기반 중복 제거를 합니다. |
managed-mcp.json이 존재하면 addMcpConfig() 호출은 즉시 예외를 던집니다. "enterprise MCP configuration is active and has exclusive control"라는 메시지가 나오며, claude mcp add 같은 도구는 완전히 막힙니다.
코드: 스코프 우선순위 조합(getAllMcpConfigs 단순화)
// services/mcp/config.ts, 개념적인 병합 순서
const allConfigs = {
...enterpriseServers, // 있으면 우선권 차지
...dynamicServers, // --mcp-config 플래그
...claudeAiServers, // URL 시그니처로 중복 제거
...projectServers, // .mcp.json, 루트부터 아래로
...localServers, // ~/.claude/projects/…
...userServers, // ~/.claude/settings.json
...pluginServers, // 이름공간 부여, 시그니처로 중복 제거
}
// 모든 설정에서 연결 전에 환경 변수를 확장
// 예: command: "npx", args: ["$MY_SERVER_PATH"]
정책 허용/차단 목록
Enterprise 정책은 설정에서 allowedMcpServers와 deniedMcpServers를 정의할 수 있습니다. 차단 목록이 절대 우선합니다. 일치는 다음 기준으로 판단할 수 있습니다.
- 이름, 서버 이름 문자열과 정확히 일치
- 명령, stdio 서버의 전체 command+args 배열
- URL 패턴, 원격 서버용
*와일드카드를 쓰는 glob
4 연결 수명 주기
모든 서버는 MCPServerConnection 유니언 타입이 관리하는 상태 머신을 거칩니다.
설정 조합
모든 스코프를 병합하고 정책 필터링을 적용합니다. 환경 변수도 확장합니다($VAR / ${VAR}). 빠진 변수는 경고로 기록되지만 연결 자체를 막지는 않습니다.
배치 연결
stdio 서버는 3개씩 배치로 연결합니다(MCP_SERVER_CONNECTION_BATCH_SIZE). 원격 서버는 20개씩입니다. connectToServer() 호출은 각각 name + JSON(config) 기준으로 메모이즈됩니다.
전송 객체 구성
serverRef.type에 따라 올바른 SDK transport 클래스가 인스턴스화됩니다. 인증 provider, 프록시 에이전트, mTLS 옵션도 여기서 붙습니다.
타임아웃이 있는 client.connect()
기본값은 30초입니다(MCP_TIMEOUT 환경 변수). connectPromise와 timeoutPromise를 경쟁시킵니다. 타임아웃이 나면 시작된 in-process 서버도 함께 닫습니다.
인증 처리
UnauthorizedError (401)가 나면 서버는 needs-auth 상태로 이동합니다. 모델이 OAuth를 트리거할 수 있게 McpAuthTool 의사 도구를 주입합니다. 15분 뒤에는 needs-auth 캐시 항목이 만료됩니다.
capability 협상
Claude Code는 roots: {}와 elicitation: {} capability를 선언합니다. 서버 capability는 getServerCapabilities()로 읽습니다. 서버 instructions는 2048자로 잘립니다.
tool/resource/prompt 가져오기
tools, resources, prompts를 병렬로 가져옵니다. 도구 이름은 정규화됩니다(mcp__server__tool). 각 도구는 name, description, inputSchema, call()이 덮어써진 복제 MCPTool입니다.
실시간 알림
ToolListChanged, ResourceListChanged, PromptListChanged 알림을 구독합니다. 바뀐 내용이 있으면 다시 가져오고 AppState를 업데이트합니다. 재연결은 지수 백오프를 쓰며 1초에서 시작해 30초 상한, 최대 5번 시도합니다.
코드: 타임아웃 경쟁을 쓰는 연결
// services/mcp/client.ts (단순화)
const connectPromise = client.connect(transport)
const timeoutPromise = new Promise<never>((_, reject) => {
const id = setTimeout(() => {
transport.close().catch(() => {})
reject(new Error(`MCP server "${name}" timed out after ${timeout}ms`))
}, getConnectionTimeoutMs())
connectPromise.then(() => clearTimeout(id), () => clearTimeout(id))
})
await Promise.race([connectPromise, timeoutPromise])
5 도구 프록시 처리
MCPTool은 tools/MCPTool/MCPTool.ts에 정의된 템플릿입니다. 연결된 서버가 보고한 각 도구마다 client.ts의 fetchToolsForClient()가 실제 메타데이터를 덮어쓴 깊은 복제본을 만듭니다.
tool.description
inputSchema
tool_name
desc, schema
call()
Claude가
사용 가능
→ JSON-RPC
→ transport
이름 정규화
// services/mcp/normalization.ts
export function normalizeNameForMCP(name: string): string {
// [a-zA-Z0-9_-]에 없는 문자는 모두 언더스코어로 바꾼다
let normalized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
// claude.ai 서버는 연속된 언더스코어를 줄이고,
// 앞뒤 언더스코어를 제거한다 (__ 구분자는 깔끔해야 함)
if (name.startsWith('claude.ai ')) {
normalized = normalized.replace(/_+/g, '_').replace(/^_|_$/g, '')
}
return normalized
}
// 전체 도구 이름: "mcp__my_server__list_files"
export function buildMcpToolName(server: string, tool: string): string {
return `mcp__${normalizeNameForMCP(server)}__${normalizeNameForMCP(tool)}`
}
tool.description에 15~60 KB짜리 문서를 쏟아 넣는 경우가 있습니다. Claude Code는 컨텍스트 창이 감당 가능하도록 도구 설명과 서버 instructions를 모두 2048자로 강하게 제한합니다.
코드: 결과 처리, 이미지, 바이너리 blob, 잘라내기
// client.callTool()이 CallToolResult를 반환한 뒤...
// 1. 이미지 콘텐츠 항목, 크기 조정/다운샘플링 후 base64로 반환
if (content.type === 'image' && IMAGE_MIME_TYPES.has(mimeType)) {
const buf = maybeResizeAndDownsampleImageBuffer(rawBuf)
// → base64 데이터가 담긴 ContentBlockParam
}
// 2. 바이너리 blob, 디스크에 저장하고 경로를 텍스트로 반환
if (!IMAGE_MIME_TYPES.has(mimeType)) {
await persistBinaryContent(content)
// → getBinaryBlobSavedMessage(path)
}
// 3. 전체 결과가 100 KB 초과, 안내 문구와 함께 잘라냄
if (mcpContentNeedsTruncation(result)) {
result = truncateMcpContentIfNeeded(result)
}
6 OAuth 인증
SSE 또는 HTTP 전송을 쓰는 MCP 서버는 OAuth를 요구할 수 있습니다. services/mcp/auth.ts의 인증 시스템은 XAA(Cross-App Access) 확장 지원을 포함한 전체 PKCE 흐름을 구현합니다.
needs-auth 상태에 들어가면 mcp__<server>__authenticate라는 이름의 의사 도구가 주입됩니다. 모델은 이를 호출해서 OAuth 흐름을 시작하고, 사용자에게 보여 줄 인증 URL을 받을 수 있습니다. callback이 실행되면 실제 도구가 의사 도구를 자동으로 대체합니다(AppState에서 prefix 기반 교체).
토큰 갱신과 Slack의 특이점
표준 RFC 6749의 invalid_grant 오류는 토큰 무효화를 일으킵니다. 하지만 Slack은 HTTP 200과 함께 {"error":"invalid_refresh_token"}를 반환하고, SDK는 이를 ZodError로 보게 됩니다. Claude Code는 이런 비표준 코드를 먼저 invalid_grant로 정규화한 뒤 SDK의 error-class mapper로 넘깁니다.
코드: Slack 200-오류 정규화
// services/mcp/auth.ts
const NONSTANDARD_INVALID_GRANT_ALIASES = new Set([
'invalid_refresh_token',
'expired_refresh_token',
'token_expired',
])
// fetch를 감싼다. 2xx POST 응답을 들여다보고 오류 본문을 다시 쓴다
// OAuthErrorResponseSchema에는 맞지만 OAuthTokensSchema에는 맞지 않으면
// 가짜 400 응답으로 바꿔서 SDK의 오류 클래스 매핑이 적용되게 한다.
XAA, Cross-App Access
SSO 흐름을 위한 Enterprise 확장입니다. MCP 서버 설정에 xaa: true가 있으면 시스템은 브라우저를 띄우는 대신 IdP ID-token을 MCP 서버의 OAuth 토큰으로 조용히 교환합니다. 한 번 settings.xaaIdp에 설정하면 XAA가 켜진 모든 서버가 공유합니다.
7 Elicitation
Elicitation은 서버가 작업 도중 사용자에게 구조화된 입력을 요청할 때 쓰는 MCP 메커니즘입니다. Claude Code는 두 모드를 모두 지원합니다.
서버가 JSON Schema를 보내면 사용자가 폼을 채웁니다. 응답은 내용이 담긴 accept이거나, decline / cancel입니다.
서버가 URL을 보냅니다(예: OAuth step-up, 외부 확인). 두 단계로 진행됩니다. URL 열기, 그 다음 ElicitationComplete 알림을 기다립니다. 사용자는 닫거나 다시 시도할 수 있습니다.
요청은 respond() 콜백이 달린 ElicitationRequestEvent 객체로 AppState.elicitation.queue에 들어갑니다. React 컴포넌트 ElicitationDialog가 이 큐를 폴링합니다. hooks(executeElicitationHooks)는 UI를 띄우지 않고도 프로그래밍적으로 요청을 처리할 수 있습니다.
코드: elicitation 핸들러 등록
// services/mcp/elicitationHandler.ts
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
// 1. 먼저 hooks를 시도(프로그래밍 방식 응답)
const hookResponse = await runElicitationHooks(serverName, request.params, extra.signal)
if (hookResponse) return hookResponse
// 2. 사용자 상호작용을 위해 큐에 넣기
const response = new Promise<ElicitResult>(resolve => {
setAppState(prev => ({
...prev,
elicitation: {
queue: [...prev.elicitation.queue, {
serverName, requestId: extra.requestId,
params: request.params,
respond: resolve,
}],
},
}))
})
return await response
})
8 서버 중복 제거
여러 설정 소스가 같은 실제 서버를 제공할 때 Claude Code는 이름만이 아니라 내용 시그니처를 기준으로 중복 제거합니다. stdio 서버의 시그니처는 stdio:["cmd","arg1"]이고, 원격 서버는 url:https://vendor.example.com/mcp입니다.
mcp_url 쿼리 파라미터에 보존됩니다. unwrapCcrProxyUrl()는 시그니처 비교 전에 이를 꺼내기 때문에, Slack의 MCP 서버를 직접 가리키는 플러그인도 CCR을 거친 claude.ai 커넥터와 올바르게 중복 제거됩니다.
중복 제거 규칙은 다음과 같습니다.
- 수동 설정이 플러그인보다 우선, 사용자 설정 서버가 항상 플러그인 제공 서버를 이깁니다.
- 먼저 로드된 플러그인이 우선, 두 플러그인이 같은 서버를 제공하면 먼저 로드된 쪽이 이깁니다.
- 활성화된 수동 설정이 claude.ai보다 우선, 비활성화된 수동 서버는 해당 커넥터 쌍을 억누르지 않습니다. 그러면 둘 다 실행되지 않게 되기 때문입니다.
- 플러그인 서버는 중복 제거 전부터 키 충돌을 피하려고
plugin:name:server이름공간을 사용합니다.
핵심 요약
- 전송 유형은 7가지, stdio, sse, http, ws, sse-ide, ws-ide, sdk가 있고 여기에 Chrome/Computer Use용 내부 in-process 쌍이 더해집니다. public 전송은 OAuth를 지원하고, IDE 변형은 인증이 없거나 토큰 기반입니다.
- 스코프 우선순위는 7단계, enterprise > dynamic > claudeai > project > local > user > managed 순서입니다. Enterprise 설정이 있으면 수동 추가/삭제 작업은 모두 잠깁니다.
- 설정 파일은 디렉터리 트리를 따라 탐색,
.mcp.json은 파일시스템 루트까지 모든 상위 디렉터리에서 읽고, 하위 디렉터리가 상위를 덮어씁니다. - 모든 도구 이름은 정규화를 거침,
mcp__<server>__<tool>형식을 쓰며 영숫자가 아닌 문자는 모두 언더스코어로 바뀝니다. Claude.ai 서버 이름은__구분자를 보호하려고 추가로 축약/제거 처리를 합니다. - OAuth는 모델이 시작할 수 있음,
McpAuthTool의사 도구 덕분에 Claude가 스스로 인증 흐름을 시작하고 URL을 사용자에게 돌려줄 수 있습니다. callback 뒤에는 실제 도구가 자동으로 대체됩니다. - 도구 설명은 2048자로 강하게 제한, OpenAPI 생성 서버가 컨텍스트를 과도하게 부풀리는 일을 막습니다.
- 중복 제거는 이름이 아니라 내용 기준, command 배열이나 URL이 같으면 서로 다른 설정 소스에서 다른 이름으로 불려도 같은 서버로 봅니다.
퀴즈 — 5문제
.mcp.json(scope: project)과 ~/.claude/settings.json(scope: user) 양쪽에 모두 추가했습니다. 어느 설정이 우선할까요?stderr 출력이 Claude Code UI에 직접 잡음을 뿌리고 있습니다. 코드는 이를 어떻게 막을까요?wrapFetchWithTimeout()의 목적은 무엇이고, 왜 의도적으로 GET 요청을 건너뛸까요?{"error":"invalid_refresh_token"}를 반환했습니다. Claude Code에서는 무슨 일이 일어날까요?managed-mcp.json 파일이 존재합니다. 사용자가 claude mcp add my-tool --command npx my-tool를 실행하면 어떻게 될까요?doesEnterpriseMcpConfigExist()는 addMcpConfig() 시작 시점에 확인되고, 즉시 예외를 던집니다. Enterprise 설정이 독점 제어권을 가집니다.