| **MCP** (Model Context Protocol) | AI 모델에 외부 도구·리소스·프롬프트 제공 | 양방향 (클라이언트 ↔ 서버) | Anthropic MCP Spec 2025-03-26 |
| **LSP** (Language Server Protocol) | 코드 인텔리전스 (정의, 참조, 진단 등) | 클라이언트 → 서버 (단방향 요청 + 서버 알림) | Microsoft LSP 3.17 |
MCP는 Claude 모델이 사용할 수 있는 **도구(Tool)** 를 외부 서버로부터 동적으로 수급하는 메커니즘이며, LSP는 파일 편집·분석 작업 시 언어 서버로부터 **코드 인텔리전스**를 얻기 위한 메커니즘이다. 두 프로토콜은 서로 독립적으로 초기화되며, 각각 별도의 관리자(Manager) 싱글턴을 통해 생명주기가 제어된다.
```
┌─────────────────────────────────────────┐
│ Claude Code │
│ │
│ ┌───────────────┐ ┌────────────────┐ │
│ │ MCP 클라이언트 │ │ LSP 관리자 │ │
│ │ (mcpClient) │ │ (manager.ts) │ │
│ └──────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ ┌──────▼────────┐ ┌───────▼────────┐ │
│ │ MCPTool │ │ LSPTool │ │
│ │ (Tool 매핑) │ │ (Tool 래퍼) │ │
│ └───────────────┘ └────────────────┘ │
└─────────────────────────────────────────┘
│ │
MCP 서버들 LSP 서버들 (플러그인 제공)
(stdio/SSE/HTTP/WS) (stdio, vscode-jsonrpc)
```
---
## 2. MCP (Model Context Protocol) 통합
### 2.1 서버 설정 및 스코프 계층
MCP 서버 설정은 `src/services/mcp/types.ts`에 Zod 스키마로 엄밀하게 정의된다. 설정은 **스코프(scope)** 개념을 통해 계층화된다.
```typescript
// ConfigScopeSchema — 우선순위 낮은 순서
type ConfigScope =
| 'enterprise' // 관리형 정책 파일 (최고 우선순위)
| 'user' // 사용자 전역 설정
| 'project' // .mcp.json (프로젝트별)
| 'local' // 로컬 오버라이드
| 'dynamic' // 런타임 동적 추가
| 'claudeai' // claude.ai 클라우드 서버
| 'managed' // 플러그인 제공 서버
```
각 서버 항목은 `ScopedMcpServerConfig` 타입으로 래핑되어 원본 설정과 스코프가 함께 보존된다. `config.ts`의 `getAllMcpConfigs()`가 모든 소스를 병합하여 최종 서버 목록을 반환한다.
프로젝트 수준 설정은 `.mcp.json` 파일에 저장된다. `writeMcpjsonFile()` 함수는 원자적 파일 쓰기를 구현한다 — 임시 파일에 기록(`O_WRONLY`), `datasync()`로 디스크 플러시, `rename()`으로 원자적 교체. 이는 설정 파일 손상을 방지하기 위한 의도적인 설계다.
### 2.2 전송 계층 (Transport Layer)
MCP는 여섯 가지 전송 유형을 지원하며, 각각 독립적인 연결 전략을 사용한다.
```
TransportType:
stdio — 서브프로세스 stdin/stdout (로컬 서버, 가장 일반적)
sse — Server-Sent Events over HTTP (원격, OAuth 지원)
sse-ide — IDE 확장용 SSE (인증 없음)
http — Streamable HTTP (MCP Spec 2025-03-26)
ws — WebSocket
ws-ide — IDE 확장용 WebSocket (authToken 지원)
sdk — 인프로세스 (InProcessTransport)
claudeai-proxy — claude.ai 프록시 서버
```
`connectToServer()` 함수(`client.ts:595`)는 `memoize`로 래핑되어 동일한 서버에 대한 중복 연결을 방지한다. 캐시 키는 서버 이름과 설정 JSON의 조합(`getServerCacheKey()`)으로 생성된다.
**연결 타임아웃**: 기본 30초(`MCP_TIMEOUT` 환경변수로 조정 가능). `Promise.race()`를 통해 연결과 타임아웃 프로미스를 경쟁시키며, 타임아웃 발생 시 전송 계층을 강제 종료한다.
**배치 연결**: 로컬 서버(stdio/sdk)는 동시 3개, 원격 서버(SSE/HTTP/WS)는 동시 20개까지 병렬 연결한다(`pMap` 활용). 환경변수 `MCP_SERVER_CONNECTION_BATCH_SIZE`와 `MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE`로 조정 가능.
#### HTTP 전송의 특수 처리
Streamable HTTP(`type: 'http'`)는 MCP 사양 2025-03-26을 따른다. POST 요청에는 반드시 `Accept: application/json, text/event-stream` 헤더가 포함되어야 한다. `wrapFetchWithTimeout()` 함수가 이 헤더를 보장하며, 동시에 60초 타임아웃을 적용한다.
```typescript
// GET 요청(SSE 스트림)은 타임아웃에서 제외 — 장시간 유지되는 연결이기 때문
if (method === 'GET') {
return baseFetch(url, init)
}
```
`AbortSignal.timeout()` 대신 `setTimeout` + `clearTimeout` 패턴을 사용한다. 이는 Bun 런타임에서 `AbortSignal.timeout()`이 GC 전까지 요청당 ~2.4KB의 네이티브 메모리를 누수하는 버그를 회피하기 위함이다.
#### 인프로세스 서버
Chrome과 Computer Use 서버는 서브프로세스 대신 **인프로세스**로 실행된다. `createLinkedTransportPair()`로 연결된 `InProcessTransport` 쌍을 통해 통신하며, ~325MB의 서브프로세스 오버헤드를 제거한다.
### 2.3 리소스 및 프롬프트
MCP 서버가 제공하는 **리소스(Resource)** 와 **프롬프트(Prompt)** 는 각각 `ListMcpResourcesTool`과 `ReadMcpResourceTool`을 통해 Claude에 노출된다.
```typescript
type ServerResource = Resource & { server: string }
// server 필드를 추가하여 리소스 출처 서버를 식별
```
`MCPCliState` 인터페이스는 연결된 클라이언트, 설정, 도구, 리소스, 정규화된 이름 매핑을 하나의 직렬화 가능한 상태로 묶는다. CLI 세션 간 상태 전달을 위한 구조다.
### 2.4 Tool 매핑
MCP 서버가 제공하는 도구는 `MCPTool` 정적 템플릿을 기반으로 **동적으로 생성**된다. `MCPTool.ts`의 정적 정의는 모두 기본값(빈 이름, 빈 설명)이며, `mcpClient.ts`에서 실제 서버의 도구 메타데이터로 오버라이드된다.
**도구 이름 정규화**: MCP 도구는 `mcp__{서버명}__{도구명}` 형식의 이름을 갖는다. `normalizeNameForMCP()` 함수가 API 패턴 `^[a-zA-Z0-9_-]{1,64}$`에 맞지 않는 문자를 언더스코어로 치환한다.
```typescript
// normalization.ts
export function normalizeNameForMCP(name: string): string {
let normalized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
**세대 카운터(Generation Counter)**: `initializationGeneration` 값을 증가시켜 진행 중인 초기화 프로미스를 무효화한다. 플러그인 새로고침(`reinitializeLspServerManager()`) 시 이전 초기화 결과가 새 상태를 덮어쓰는 것을 방지한다.
**플러그인 재초기화 문제**: `loadAllPlugins()`가 메모이즈되어 있어, 마켓플레이스 조정 전에 빈 플러그인 목록으로 캐싱될 수 있다. `reinitializeLspServerManager()`는 플러그인 캐시 갱신 후 LSP를 재초기화하여 이 문제를 해결한다.
`isLspConnected()`는 `LSPTool.isEnabled()`의 게이트로, 실행 중인 서버가 하나 이상이고 `error` 상태가 아닌 경우에만 `true`를 반환한다.
### 3.3 다중 서버 라우팅 (`LSPServerManager.ts`)
`LSPServerManager`는 파일 확장자 기반 라우팅을 구현한다.
```typescript
// 확장자 → 서버명 매핑
const extensionMap: Map<string,string[]> = new Map()
function getServerForFile(filePath: string): LSPServerInstance | undefined {
동일 확장자를 처리하는 서버가 여럿일 경우 첫 번째 등록된 서버를 사용한다(우선순위 로직 추가 예정).
**열린 파일 추적**: `openedFiles: Map<string, string>` (URI → 서버명)를 통해 동일 파일의 중복 `didOpen` 알림을 방지한다.
**`workspace/configuration` 처리**: TypeScript 언어 서버 등은 `workspace/configuration` 요청을 보내지만, Claude Code는 이를 지원하지 않는다. 모든 항목에 `null`을 반환하여 프로토콜 요구사항을 충족시킨다.
**셧다운 전략**: `running` 또는 `error` 상태의 서버만 명시적으로 중지한다. `Promise.allSettled()`를 사용하여 일부 서버 중지 실패가 나머지 서버 정리를 방해하지 않도록 한다.
### 3.4 단일 서버 인스턴스 (`LSPServerInstance.ts`)
서버 인스턴스의 상태 머신:
```
stopped → starting → running
running → stopping → stopped
any → error (크래시/실패)
error → starting (재시도, maxRestarts 상한 있음)
```
**크래시 복구**: `createLSPClient()`의 `onCrash` 콜백으로 크래시를 감지하여 `state = 'error'`로 전환한다. 다음 요청 시 `ensureServerStarted()`가 자동으로 재시작을 시도한다. `config.maxRestarts`(기본값 3)를 초과하면 `Error`를 던지고 재시도를 포기한다.
**일시적 에러 재시도**: LSP 에러 코드 `-32801`("Content Modified")은 서버가 아직 인덱싱 중일 때 발생하는 일시적 에러다. 최대 3회, 500ms/1000ms/2000ms 지수 백오프로 재시도한다.
**지연 로딩(Lazy Loading)**: `vscode-jsonrpc`(~129KB)는 LSP 서버가 실제로 인스턴스화될 때까지 로드하지 않는다. `require('./LSPClient.js')`를 런타임에 호출하여 정적 임포트 체인에서 제외한다.
### 3.5 JSON-RPC 연결 (`LSPClient.ts`)
`createLSPClient()`는 `child_process.spawn()`으로 LSP 서버 프로세스를 시작하고, `vscode-jsonrpc`의 `createMessageConnection()`으로 stdio 기반 JSON-RPC 연결을 수립한다.
**스폰 경쟁 조건 처리**: `spawn()` 반환 직후 스트림을 사용하면 `ENOENT`(명령어 없음) 에러가 비동기적으로 발생하여 처리되지 않은 프로미스 거부가 생길 수 있다. 이를 방지하기 위해 `'spawn'` 이벤트를 기다린 후에 스트림을 사용한다.
-`inputSchema`는 `passthrough()`를 사용하여 MCP 서버가 정의하는 임의의 입력 구조를 허용
- 권한 검사는 항상 `passthrough` — MCP 도구의 권한은 서버 수준에서 관리됨
-`isOpenWorld(): false` — 알려진 도구 집합 내에서만 동작
-`classifyForCollapse` 및 `renderToolUseProgressMessage`를 통해 UI에 진행 상태 표시 지원
### 4.2 LSPTool
`LSPTool`(`src/tools/LSPTool/LSPTool.ts`)은 LSP 기능을 Claude의 도구 인터페이스로 노출하는 **단일 도구**다. 여러 LSP 작업을 하나의 도구로 통합하여 입력 `operation` 필드로 구분한다.
```typescript
export const LSPTool = buildTool({
name: LSP_TOOL_NAME,
isLsp: true,
shouldDefer: true, // 초기화 완료 대기
isEnabled() { return isLspConnected() }, // 동적 활성화 상태
isConcurrencySafe() { return true }, // 병렬 실행 안전
isReadOnly() { return true }, // 파일 수정 없음
})
```
**`shouldDefer: true`**: 도구 실행 전 LSP 초기화 완료를 기다린다. `waitForInitialization()`을 호출하여 `pending` 상태가 해소될 때까지 블로킹한다.
**입력 스키마 이중 검증**: `z.strictObject()`로 1차 검증 후, `lspToolInputSchema()` 판별 유니온으로 2차 검증하여 더 정확한 에러 메시지를 제공한다. 결과 포맷팅은 `formatters.ts`의 작업별 함수(`formatHoverResult`, `formatGoToDefinitionResult` 등)가 담당한다.
**`symbolContext.ts`**: 심볼 컨텍스트를 보강하는 유틸리티. 정의 위치 주변 코드를 포함하여 Claude가 더 풍부한 컨텍스트를 얻을 수 있도록 한다.
---
## 5. 설계 결정
### 5.1 MCP 도구의 동적 생성 vs. 정적 정의
MCP 도구는 서버에 연결하기 전까지 스키마를 알 수 없다. `MCPTool`이 정적 템플릿이고 실제 도구가 런타임에 생성되는 이유다. `buildTool()`이 반환하는 `ToolDef`를 기반으로, `mcpClient.ts`는 각 서버의 `tools/list` 응답을 받아 도구 인스턴스를 동적으로 구성한다.
### 5.2 LSP가 플러그인 전용인 이유
LSP 서버는 언어별 바이너리(`typescript-language-server`, `rust-analyzer` 등)를 실행한다. 이 바이너리들은 사용자 환경에 설치되어 있어야 하며, 플러그인이 이 의존성 확인과 경로 설정을 캡슐화하기에 적합하다. 직접 설정을 허용하면 잘못된 설정으로 인한 크래시 루프 가능성이 높아진다.
### 5.3 연결 캐시와 세션 만료
`connectToServer()`의 `memoize`는 동일 설정에 대해 하나의 연결만 유지한다. Streamable HTTP 서버의 세션 만료(HTTP 404 + JSON-RPC `-32001`)가 감지되면 캐시를 무효화하고 재연결한다. 일반 HTTP 404(잘못된 URL, 서버 다운)와 구분하기 위해 응답 본문의 JSON-RPC 에러 코드를 추가로 확인한다.
### 5.4 인증 상태의 캐싱
OAuth 인증 필요 상태를 15분간 캐싱하는 이유: 인증이 필요한 서버에 매 요청마다 재연결을 시도하면 30+ 커넥터가 동시에 401을 반환하여 인증 루프에 빠질 수 있다. 캐시는 이 폭발적 재시도를 완충한다.
### 5.5 Bun 런타임 특수 처리
일부 API(`WebSocket`, `AbortSignal.timeout`)는 Bun과 Node.js 간 동작이 다르다. `typeof Bun !== 'undefined'` 조건부 분기로 런타임을 감지하여 각각에 최적화된 구현을 선택한다.
### 5.6 도구 결과 크기 제어
MCP 도구 결과의 최대 크기는 `maxResultSizeChars: 100_000`이다. 이를 초과하는 결과는 `truncateMcpContentIfNeeded()`로 잘라내거나 `persistBinaryContent()`로 파일 시스템에 저장한다. 이진 데이터(이미지 등)는 base64 인코딩 크기를 추정하여 별도 처리한다.