연결 풀 관리
McpService는 MCP (Model Context Protocol) 모듈의 핵심 서비스로, 모든 MCP 서버의 연결 생명주기를 관리합니다. 지연 로딩 전략(처음 사용 시에만 연결 수립)을 사용하는 클라이언트 연결 풀을 유지하며, 헬스 체크 및 자동 재연결 기능을 제공합니다.
파일 경로: packages/desktop/app/main/services/capabilities/tools/mcp-users/McpService.ts
클래스 구조
class McpService extends EventEmitter {
private clients: Map<string, Client>;
private connectionStates: Map<string, ConnectionState>;
private toolsCache: Map<string, { tools: MCPTool[]; timestamp: number }>;
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
constructor(private logger: LoggerService);
}
내부 상태
| 속성 | 타입 | 설명 |
|---|---|---|
clients | Map<string, Client> | 서버 ID → MCP Client 인스턴스 매핑 |
connectionStates | Map<string, ConnectionState> | 서버 ID → 연결 상태 매핑 |
toolsCache | Map<string, { tools, timestamp }> | 서버 ID → 도구 목록 캐시 (타임스탬프 포함) |
연결 상태
stateDiagram-v2
[*] --> disconnected
disconnected --> connecting: initClient()
connecting --> connected: Connection successful
connecting --> error: Connection failed
connected --> disconnected: disconnect()
error --> connecting: reconnect()
connected --> connecting: reconnect()
| 상태 | 설명 |
|---|---|
disconnected | 연결되지 않았거나 이미 끊어진 상태 |
connecting | 연결 수립 중 |
connected | 연결 활성 상태 |
error | 연결 실패 상태 |
연결 생명주기
initClient — 클라이언트 초기화
initClient(server: McpServerRecord): Promise<Client>
핵심 연결 메서드의 실행 흐름:
- 재사용 확인 —
clientsMap에 해당 ID의 클라이언트가 이미 존재하면 먼저 헬스 체크 수행 - 헬스 체크 —
client.listTools()를 호출하여 연결이 유효한지 확인 - 비정상 처리 — 헬스 체크 실패 시 연결을 끊고 재생성
- 전송 계층 생성 —
server.type에 따라 해당 Transport 생성 - 클라이언트 생성 — 설정된 capabilities로 MCP SDK Client 인스턴스 생성
- 연결 수립 —
client.connect(transport)호출 - 클라이언트 캐시 —
clientsMap에 저장 - 이벤트 발행 —
server:connected이벤트 발행
const client = new Client(
{ name: 'elftia', version: '1.0.0' },
{
capabilities: {
roots: { listChanged: true },
sampling: {}
}
}
);
await client.connect(transport);
disconnect — 연결 해제
disconnect(serverId: string): Promise<void>
- Client 인스턴스 가져오기
client.close()호출 (오류 처리 포함)clientsMap에서 제거- 상태를
disconnected로 설정
reconnect — 재연결
reconnect(serverId: string): Promise<void>
먼저 disconnect()를 호출한 후, 서버가 isActive이면 initClient()를 호출합니다.
cleanup — 전체 정리
cleanup(): Promise<void>
모든 연결을 해제하고 clients, connectionStates, toolsCache 세 개의 Map을 초기화합니다. 애플리케이션 종료 시 사용됩니다.
전송 계층 구현
StdioClientTransport
로컬 프로세스 통신에 사용되며, 표준 입출력(stdin/stdout)을 통해 자식 프로세스와 통신합니다.
new StdioClientTransport({
command: server.config.command,
args: server.config.args || [],
env: {
...process.env, // 시스템 환경 상속
...server.config.env // 사용자 정의 변수로 덮어쓰기
}
});
특징:
- 자식 프로세스 자동 시작
- 환경 변수는 현재 프로세스를 완전히 상속하고 사용자 정의 설정으로 오버레이
- 프로세스 종료 시 연결 자동 닫힘
SSEClientTransport
HTTP Server-Sent Events 기반의 장기 연결 전송으로, Node.js 기본 fetch 대신 Electron의 net.fetch를 사용합니다.
new SSEClientTransport(new URL(server.config.url), {
fetch: (url, init) => {
return net.fetch(
typeof url === 'string' ? url : url.toString(),
init
);
},
requestInit: {
headers: server.config.headers || {}
}
});
net.fetch를 사용하는 이유:
- Electron의
net모듈은 Chromium 네트워크 스택을 따름 - 더 나은 SSL/TLS 지원 및 인증서 관리
- 시스템 프록시 설정 자동 사용
StreamableHTTPClientTransport
MCP 2025-03-26 명세 기반의 Streamable HTTP 전송입니다. 구성은 SSE와 유사합니다:
new StreamableHTTPClientTransport(new URL(server.config.url), {
fetch: (url, init) => {
return net.fetch(
typeof url === 'string' ? url : url.toString(),
init
);
},
requestInit: {
headers: server.config.headers || {}
}
});
구성 저장
MCP 서버 구성은 Electron Store에 저장됩니다:
const configStore = new Store<{
mcpServers: McpServerRecord[];
}>({
name: 'mcp-config',
defaults: { mcpServers: [] }
});
파일 위치: {userData}/mcp-config.json
McpServerRecord 전체 필드
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
id | string | 자동 생성 | 고유 식별자, 형식 mcp_{timestamp}_{random} |
name | string | - | 서버 이름 (고유해야 함) |
type | McpServerTransport | - | stdio / sse / http |
scope | McpServerScope | 'user' | user (전역) / local (프로젝트 수준) |
config | McpServerConfig | - | 연결 구성 (command/args/env/url/headers) |
isActive | boolean | true | 활성화 여부 |
isTrusted | boolean | false | 신뢰 여부 |
disabledTools | string[] | [] | 비활성화된 도구 이름 목록 |
disabledAutoApproveTools | string[] | [] | 자동 승인을 허용하지 않는 도구 이름 목록 |
installSource | string | 'manual' | 설치 출처: builtin / manual / protocol / unknown |
projectPath | string? | - | 연결된 프로젝트 경로 (scope가 local일 때 사용) |
createdAt | number | Date.now() | 생성 타임스탬프 |
updatedAt | number? | - | 마지막 업데이트 타임스탬프 |
핵심 메서드
list — 서버 목록 조회
async list(): Promise<McpServerRecord[]>
configStore에서 서버 목록을 직접 읽으며, 연결을 수반하지 않습니다.
add — 서버 추가
async add(input: McpServerInput): Promise<McpActionResult>
- 이름 고유성 확인
McpServerRecord생성 및 고유 ID 생성- configStore에 저장
isActive이면 연결 수립 시도 (연결 실패가 추가를 막지 않음)servers:changed이벤트 발행
addJson — JSON으로 일괄 추가
async addJson(input: McpServerJsonInput): Promise<McpActionResult>
JSON 문자열을 파싱하고 { [name]: config } 객체를 순회하며, 각 항목에 대해 add()를 호출합니다. 집계된 결과를 반환합니다.
update — 서버 업데이트
async update(id: string, updates: Partial<McpServerRecord>): Promise<McpActionResult>
- ID로 서버 찾기
- 업데이트 병합 (ID는 변경되지 않음)
- 저장 및
updatedAt설정 config또는type이 변경된 경우reconnect()트리거servers:changed이벤트 발행
listServerTools — 서버 도구 목록 조회
async listServerTools(serverId: string): Promise<MCPTool[]>
- 캐시 히트 여부 및 만료 여부 확인
- 히트 시 바로 반환
- 미스 시
initClient()→client.listTools() MCPTool[]형식으로 변환하고disabledTools필터링- 캐시에 기록
도구 ID 생성 규칙: mcp__${cleanServerName}__${cleanToolName} (영숫자가 아닌 문자는 _로 교체)
callTool — 도구 호출
async callTool(serverId: string, toolName: string, args: any, callId: string): Promise<MCPCallToolResponse>
initClient()로 연결 보장client.callTool({ name, arguments })- 호출 소요 시간 기록
tool:called이벤트 발행{ isError, content }또는 오류 메시지 반환
test — 연결 테스트
async test(input: McpServerRemoveInput): Promise<McpTestResult>
연결 및 도구 목록 조회를 시도하고 성공/실패 여부와 도구 이름 목록을 반환합니다.
discover — 기능 탐색
async discover(input: McpServerRemoveInput): Promise<McpDiscoverResult>
연결 후 listTools(), listPrompts(), listResources()를 각각 호출하고 (후자 두 개는 선택 사항이며 부분 실패 허용), 전체 기능 목록을 반환합니다.
이벤트
McpService는 EventEmitter를 상속하며 다음 이벤트를 발행합니다:
| 이벤트 | 인수 | 트리거 |
|---|---|---|
server:connected | serverId | 서버 연결 성공 |
connection:state | serverId, state | 연결 상태 변경 |
servers:changed | 없음 | 서버 목록 변경 (추가/삭제/수정) |
tool:called | { serverId, toolName, duration, success } | 도구 호출 완료 |
확장 포인트
- 새 전송 타입 추가 —
createTransport()의 switch 문에 새 case 분기 추가 - 헬스 체크 커스터마이징 —
isClientHealthy()메서드 수정 - 캐시 전략 —
CACHE_TTL수정 또는 더 복잡한 캐시 무효화 전략 구현 - 연결 이벤트 — EventEmitter 이벤트를 구독하여 모니터링
관련 파일
- 도구 형식 어댑터 — 도구 형식 변환
- Worker 아키텍처 — CLI 브리징을 위한 Worker
- 모듈 개요 — 전체 아키텍처