본문으로 건너뛰기

연결 풀 관리

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);
}

내부 상태

속성타입설명
clientsMap<string, Client>서버 ID → MCP Client 인스턴스 매핑
connectionStatesMap<string, ConnectionState>서버 ID → 연결 상태 매핑
toolsCacheMap<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>

핵심 연결 메서드의 실행 흐름:

  1. 재사용 확인clients Map에 해당 ID의 클라이언트가 이미 존재하면 먼저 헬스 체크 수행
  2. 헬스 체크client.listTools()를 호출하여 연결이 유효한지 확인
  3. 비정상 처리 — 헬스 체크 실패 시 연결을 끊고 재생성
  4. 전송 계층 생성server.type에 따라 해당 Transport 생성
  5. 클라이언트 생성 — 설정된 capabilities로 MCP SDK Client 인스턴스 생성
  6. 연결 수립client.connect(transport) 호출
  7. 클라이언트 캐시clients Map에 저장
  8. 이벤트 발행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>
  1. Client 인스턴스 가져오기
  2. client.close() 호출 (오류 처리 포함)
  3. clients Map에서 제거
  4. 상태를 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 전체 필드

필드타입기본값설명
idstring자동 생성고유 식별자, 형식 mcp_{timestamp}_{random}
namestring-서버 이름 (고유해야 함)
typeMcpServerTransport-stdio / sse / http
scopeMcpServerScope'user'user (전역) / local (프로젝트 수준)
configMcpServerConfig-연결 구성 (command/args/env/url/headers)
isActivebooleantrue활성화 여부
isTrustedbooleanfalse신뢰 여부
disabledToolsstring[][]비활성화된 도구 이름 목록
disabledAutoApproveToolsstring[][]자동 승인을 허용하지 않는 도구 이름 목록
installSourcestring'manual'설치 출처: builtin / manual / protocol / unknown
projectPathstring?-연결된 프로젝트 경로 (scope가 local일 때 사용)
createdAtnumberDate.now()생성 타임스탬프
updatedAtnumber?-마지막 업데이트 타임스탬프

핵심 메서드

list — 서버 목록 조회

async list(): Promise<McpServerRecord[]>

configStore에서 서버 목록을 직접 읽으며, 연결을 수반하지 않습니다.

add — 서버 추가

async add(input: McpServerInput): Promise<McpActionResult>
  1. 이름 고유성 확인
  2. McpServerRecord 생성 및 고유 ID 생성
  3. configStore에 저장
  4. isActive이면 연결 수립 시도 (연결 실패가 추가를 막지 않음)
  5. servers:changed 이벤트 발행

addJson — JSON으로 일괄 추가

async addJson(input: McpServerJsonInput): Promise<McpActionResult>

JSON 문자열을 파싱하고 { [name]: config } 객체를 순회하며, 각 항목에 대해 add()를 호출합니다. 집계된 결과를 반환합니다.

update — 서버 업데이트

async update(id: string, updates: Partial<McpServerRecord>): Promise<McpActionResult>
  1. ID로 서버 찾기
  2. 업데이트 병합 (ID는 변경되지 않음)
  3. 저장 및 updatedAt 설정
  4. config 또는 type이 변경된 경우 reconnect() 트리거
  5. servers:changed 이벤트 발행

listServerTools — 서버 도구 목록 조회

async listServerTools(serverId: string): Promise<MCPTool[]>
  1. 캐시 히트 여부 및 만료 여부 확인
  2. 히트 시 바로 반환
  3. 미스 시 initClient()client.listTools()
  4. MCPTool[] 형식으로 변환하고 disabledTools 필터링
  5. 캐시에 기록

도구 ID 생성 규칙: mcp__${cleanServerName}__${cleanToolName} (영숫자가 아닌 문자는 _로 교체)

callTool — 도구 호출

async callTool(serverId: string, toolName: string, args: any, callId: string): Promise<MCPCallToolResponse>
  1. initClient()로 연결 보장
  2. client.callTool({ name, arguments })
  3. 호출 소요 시간 기록
  4. tool:called 이벤트 발행
  5. { isError, content } 또는 오류 메시지 반환

test — 연결 테스트

async test(input: McpServerRemoveInput): Promise<McpTestResult>

연결 및 도구 목록 조회를 시도하고 성공/실패 여부와 도구 이름 목록을 반환합니다.

discover — 기능 탐색

async discover(input: McpServerRemoveInput): Promise<McpDiscoverResult>

연결 후 listTools(), listPrompts(), listResources()를 각각 호출하고 (후자 두 개는 선택 사항이며 부분 실패 허용), 전체 기능 목록을 반환합니다.

이벤트

McpService는 EventEmitter를 상속하며 다음 이벤트를 발행합니다:

이벤트인수트리거
server:connectedserverId서버 연결 성공
connection:stateserverId, state연결 상태 변경
servers:changed없음서버 목록 변경 (추가/삭제/수정)
tool:called{ serverId, toolName, duration, success }도구 호출 완료

확장 포인트

  • 새 전송 타입 추가createTransport()의 switch 문에 새 case 분기 추가
  • 헬스 체크 커스터마이징isClientHealthy() 메서드 수정
  • 캐시 전략CACHE_TTL 수정 또는 더 복잡한 캐시 무효화 전략 구현
  • 연결 이벤트 — EventEmitter 이벤트를 구독하여 모니터링

관련 파일